From bf0003b1996b10fd66388d36a28342bed9884b3c Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Tue, 10 Mar 2026 02:45:21 -0400 Subject: [PATCH 01/28] int commit --- .../data/v2/stub/BigtableChannelPrimer.java | 15 +- .../v2/stub/EnhancedBigtableStubSettings.java | 52 ++++-- .../data/v2/stub/NoOpChannelPrimer.java | 12 +- .../BigtableTransportChannelProvider.java | 156 +++++++++++++----- .../bigtable/gaxx/grpc/ChannelPrimer.java | 14 +- .../gaxx/grpc/DirectAccessChecker.java | 26 +++ .../gaxx/grpc/UnaryDirectAccessChecker.java | 58 +++++++ 7 files changed, 260 insertions(+), 73 deletions(-) create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/DirectAccessChecker.java create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/UnaryDirectAccessChecker.java diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java index e770e04ccb..85adcf5772 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java @@ -26,6 +26,7 @@ import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer; import io.grpc.CallCredentials; import io.grpc.CallOptions; +import io.grpc.Channel; import io.grpc.ClientCall; import io.grpc.Deadline; import io.grpc.ManagedChannel; @@ -88,21 +89,21 @@ static BigtableChannelPrimer create( } @Override - public void primeChannel(ManagedChannel managedChannel) { + public void primeChannel(Channel channel) { try { - primeChannelUnsafe(managedChannel); + primeChannelUnsafe(channel); } catch (IOException | RuntimeException e) { LOG.log(Level.WARNING, "Unexpected error while trying to prime a channel", e); } } - private void primeChannelUnsafe(ManagedChannel managedChannel) throws IOException { - sendPrimeRequestsBlocking(managedChannel); + private void primeChannelUnsafe(Channel channel) throws IOException { + sendPrimeRequestsBlocking(channel); } - private void sendPrimeRequestsBlocking(ManagedChannel managedChannel) { + private void sendPrimeRequestsBlocking(Channel channel) { try { - sendPrimeRequestsAsync(managedChannel).get(1, TimeUnit.MINUTES); + sendPrimeRequestsAsync(channel).get(1, TimeUnit.MINUTES); } catch (Throwable e) { // TODO: Not sure if we should swallow the error here. We are pre-emptively swapping // channels if the new @@ -112,7 +113,7 @@ private void sendPrimeRequestsBlocking(ManagedChannel managedChannel) { } @Override - public ApiFuture sendPrimeRequestsAsync(ManagedChannel managedChannel) { + public ApiFuture sendPrimeRequestsAsync(Channel managedChannel) { ClientCall clientCall = managedChannel.newCall( BigtableGrpc.getPingAndWarmMethod(), diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index db33e93fec..7ced90f8ef 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -242,6 +242,25 @@ public ClientOperationSettings getPerOpSettings() { return perOpSettings; } + /** Applies common pool, message size, and keep-alive settings to the provided builder. */ + private static InstantiatingGrpcChannelProvider.Builder commonTraits( + InstantiatingGrpcChannelProvider.Builder builder) { + return builder + .setChannelPoolSettings( + ChannelPoolSettings.builder() + .setInitialChannelCount(10) + .setMinRpcsPerChannel(1) + // Keep it conservative as we scale the channel size every 1min + // and delta is 2 channels. + .setMaxRpcsPerChannel(25) + .setPreemptiveRefreshEnabled(true) + .build()) + .setMaxInboundMessageSize(MAX_MESSAGE_SIZE) + .setKeepAliveTime(Duration.ofSeconds(30)) // sends ping in this interval + .setKeepAliveTimeout( + Duration.ofSeconds(10)); // wait this long before considering the connection dead + } + /** Returns a builder for the default ChannelProvider for this service. */ public static InstantiatingGrpcChannelProvider.Builder defaultGrpcTransportProviderBuilder() { InstantiatingGrpcChannelProvider.Builder grpcTransportProviderBuilder = @@ -261,22 +280,27 @@ public static InstantiatingGrpcChannelProvider.Builder defaultGrpcTransportProvi Collections.singletonList(InstantiatingGrpcChannelProvider.HardBoundTokenTypes.ALTS)); } } - return grpcTransportProviderBuilder - .setChannelPoolSettings( - ChannelPoolSettings.builder() - .setInitialChannelCount(10) - .setMinRpcsPerChannel(1) - // Keep it conservative as we scale the channel size every 1min - // and delta is 2 channels. - .setMaxRpcsPerChannel(25) - .setPreemptiveRefreshEnabled(true) - .build()) - .setMaxInboundMessageSize(MAX_MESSAGE_SIZE) - .setKeepAliveTime(Duration.ofSeconds(30)) // sends ping in this interval - .setKeepAliveTimeout( - Duration.ofSeconds(10)); // wait this long before considering the connection dead + return commonTraits(grpcTransportProviderBuilder); } + /** Applies Direct Access traits (DirectPath & ALTS) to an existing builder. */ + public static InstantiatingGrpcChannelProvider.Builder applyDirectAccessTraits( + InstantiatingGrpcChannelProvider.Builder builder) { + + builder + .setAttemptDirectPathXds() + .setAttemptDirectPath(true) + .setAllowNonDefaultServiceAccount(true); + + if (!DIRECT_PATH_BOUND_TOKEN_DISABLED) { + builder.setAllowHardBoundTokenTypes( + Collections.singletonList(InstantiatingGrpcChannelProvider.HardBoundTokenTypes.ALTS)); + } + + return builder; + } + + @SuppressWarnings("WeakerAccess") public static TransportChannelProvider defaultTransportChannelProvider() { return defaultGrpcTransportProviderBuilder().build(); diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/NoOpChannelPrimer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/NoOpChannelPrimer.java index 3cb98d9dee..86c564bcc0 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/NoOpChannelPrimer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/NoOpChannelPrimer.java @@ -16,11 +16,11 @@ package com.google.cloud.bigtable.data.v2.stub; import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; import com.google.api.core.InternalApi; -import com.google.api.core.SettableApiFuture; import com.google.bigtable.v2.PingAndWarmResponse; import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer; -import io.grpc.ManagedChannel; +import io.grpc.Channel; @InternalApi public class NoOpChannelPrimer implements ChannelPrimer { @@ -31,14 +31,12 @@ static NoOpChannelPrimer create() { private NoOpChannelPrimer() {} @Override - public void primeChannel(ManagedChannel channel) { + public void primeChannel(Channel channel) { // No op } @Override - public ApiFuture sendPrimeRequestsAsync(ManagedChannel channel) { - SettableApiFuture future = SettableApiFuture.create(); - future.set(PingAndWarmResponse.getDefaultInstance()); - return future; + public ApiFuture sendPrimeRequestsAsync(Channel channel) { + return ApiFutures.immediateFuture(PingAndWarmResponse.getDefaultInstance()); } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java index b75f145108..facb28dcf8 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java @@ -16,7 +16,6 @@ package com.google.cloud.bigtable.gaxx.grpc; import com.google.api.core.InternalApi; -import com.google.api.gax.grpc.ChannelFactory; import com.google.api.gax.grpc.ChannelPoolSettings; import com.google.api.gax.grpc.GrpcTransportChannel; import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; @@ -24,12 +23,19 @@ import com.google.api.gax.rpc.TransportChannelProvider; import com.google.auth.Credentials; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.ChannelPoolMetricsTracer; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; +import com.google.cloud.bigtable.data.v2.stub.BigtableChannelFactory; +import com.google.cloud.bigtable.data.v2.stub.DirectAccessChecker; +import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStubSettings; +import com.google.cloud.bigtable.data.v2.stub.UnaryDirectAccessChecker; import com.google.common.base.Preconditions; import io.grpc.ManagedChannel; import java.io.IOException; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nullable; /** @@ -38,20 +44,25 @@ */ @InternalApi public final class BigtableTransportChannelProvider implements TransportChannelProvider { + private static final Logger LOG = + Logger.getLogger(BigtableTransportChannelProvider.class.getName()); private final InstantiatingGrpcChannelProvider delegate; private final ChannelPrimer channelPrimer; @Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer; @Nullable private final ScheduledExecutorService backgroundExecutor; + DirectPathCompatibleTracer directPathCompatibleTracer; private BigtableTransportChannelProvider( - InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, - ChannelPrimer channelPrimer, - @Nullable ChannelPoolMetricsTracer channelPoolMetricsTracer, - @Nullable ScheduledExecutorService backgroundExecutor) { + InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, + ChannelPrimer channelPrimer, + @Nullable ChannelPoolMetricsTracer channelPoolMetricsTracer, + @Nullable ScheduledExecutorService backgroundExecutor, + DirectPathCompatibleTracer directPathCompatibleTracer) { delegate = Preconditions.checkNotNull(instantiatingGrpcChannelProvider); this.channelPrimer = channelPrimer; this.channelPoolMetricsTracer = channelPoolMetricsTracer; this.backgroundExecutor = backgroundExecutor; + this.directPathCompatibleTracer = directPathCompatibleTracer; } @Override @@ -66,7 +77,6 @@ public boolean needsExecutor() { } @Override - @Deprecated public BigtableTransportChannelProvider withExecutor(ScheduledExecutorService executor) { return withExecutor((Executor) executor); } @@ -76,9 +86,13 @@ public BigtableTransportChannelProvider withExecutor(ScheduledExecutorService ex @Override public BigtableTransportChannelProvider withExecutor(Executor executor) { InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withExecutor(executor); + (InstantiatingGrpcChannelProvider) delegate.withExecutor(executor); return new BigtableTransportChannelProvider( - newChannelProvider, channelPrimer, channelPoolMetricsTracer, backgroundExecutor); + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); } @Override @@ -89,9 +103,13 @@ public boolean needsBackgroundExecutor() { @Override public TransportChannelProvider withBackgroundExecutor(ScheduledExecutorService executor) { InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withBackgroundExecutor(executor); + (InstantiatingGrpcChannelProvider) delegate.withBackgroundExecutor(executor); return new BigtableTransportChannelProvider( - newChannelProvider, channelPrimer, channelPoolMetricsTracer, executor); + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + executor, + directPathCompatibleTracer); } @Override @@ -102,9 +120,13 @@ public boolean needsHeaders() { @Override public BigtableTransportChannelProvider withHeaders(Map headers) { InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withHeaders(headers); + (InstantiatingGrpcChannelProvider) delegate.withHeaders(headers); return new BigtableTransportChannelProvider( - newChannelProvider, channelPrimer, channelPoolMetricsTracer, backgroundExecutor); + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); } @Override @@ -115,9 +137,13 @@ public boolean needsEndpoint() { @Override public TransportChannelProvider withEndpoint(String endpoint) { InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withEndpoint(endpoint); + (InstantiatingGrpcChannelProvider) delegate.withEndpoint(endpoint); return new BigtableTransportChannelProvider( - newChannelProvider, channelPrimer, channelPoolMetricsTracer, backgroundExecutor); + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); } @Deprecated @@ -130,14 +156,50 @@ public boolean acceptsPoolSize() { @Override public TransportChannelProvider withPoolSize(int size) { InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withPoolSize(size); + (InstantiatingGrpcChannelProvider) delegate.withPoolSize(size); return new BigtableTransportChannelProvider( - newChannelProvider, channelPrimer, channelPoolMetricsTracer, backgroundExecutor); + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); } + // We need this for direct access checker. + /** Expected to only be called once when BigtableClientContext is created */ @Override public TransportChannel getTransportChannel() throws IOException { + InstantiatingGrpcChannelProvider directAccessProvider = + EnhancedBigtableStubSettings.applyDirectAccessTraits(delegate.toBuilder()) + .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) + .build(); + + BigtableChannelFactory maybeDirectAccessChannelFactory = + () -> { + GrpcTransportChannel channel = + (GrpcTransportChannel) directAccessProvider.getTransportChannel(); + return (ManagedChannel) channel.getChannel(); + }; + + DirectAccessChecker directAccessChecker = UnaryDirectAccessChecker.create(channelPrimer); + boolean isDirectAccessEligible = false; + + try { + isDirectAccessEligible = + directAccessChecker.check(maybeDirectAccessChannelFactory, directPathCompatibleTracer); + } catch (Exception e) { + LOG.log(Level.FINE, "Client is not direct access eligible, using standard transport.", e); + } + + InstantiatingGrpcChannelProvider selectedProvider; + + if (isDirectAccessEligible) { + selectedProvider = directAccessProvider; + } else { + selectedProvider = delegate; + } + // This provider's main purpose is to replace the default GAX ChannelPool // with a custom BigtableChannelPool, reusing the delegate's configuration. @@ -145,30 +207,32 @@ public TransportChannel getTransportChannel() throws IOException { // We achieve this by configuring our delegate to not use its own pooling // (by setting pool size to 1) and then calling getTransportChannel() on it. InstantiatingGrpcChannelProvider singleChannelProvider = - delegate.toBuilder().setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)).build(); - - ChannelFactory channelFactory = - () -> { - try { - GrpcTransportChannel channel = - (GrpcTransportChannel) singleChannelProvider.getTransportChannel(); - return (ManagedChannel) channel.getChannel(); - } catch (IOException e) { - throw new java.io.UncheckedIOException(e); - } - }; + selectedProvider.toBuilder() + .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) + .build(); + + BigtableChannelFactory channelFactory = + () -> { + try { + GrpcTransportChannel channel = + (GrpcTransportChannel) singleChannelProvider.getTransportChannel(); + return (ManagedChannel) channel.getChannel(); + } catch (IOException e) { + throw new java.io.UncheckedIOException(e); + } + }; BigtableChannelPoolSettings btPoolSettings = - BigtableChannelPoolSettings.copyFrom(delegate.getChannelPoolSettings()); + BigtableChannelPoolSettings.copyFrom(delegate.getChannelPoolSettings()); BigtableChannelPool btChannelPool = - BigtableChannelPool.create( - btPoolSettings, channelFactory, channelPrimer, backgroundExecutor); + BigtableChannelPool.create( + btPoolSettings, channelFactory, channelPrimer, backgroundExecutor); if (channelPoolMetricsTracer != null) { channelPoolMetricsTracer.registerChannelInsightsProvider(btChannelPool); channelPoolMetricsTracer.registerLoadBalancingStrategy( - btPoolSettings.getLoadBalancingStrategy()); + btPoolSettings.getLoadBalancingStrategy()); } return GrpcTransportChannel.create(btChannelPool); @@ -187,21 +251,27 @@ public boolean needsCredentials() { @Override public TransportChannelProvider withCredentials(Credentials credentials) { InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withCredentials(credentials); + (InstantiatingGrpcChannelProvider) delegate.withCredentials(credentials); return new BigtableTransportChannelProvider( - newChannelProvider, channelPrimer, channelPoolMetricsTracer, backgroundExecutor); + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); } /** Creates a BigtableTransportChannelProvider. */ public static BigtableTransportChannelProvider create( - InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, - ChannelPrimer channelPrimer, - ChannelPoolMetricsTracer outstandingRpcsMetricTracker, - ScheduledExecutorService backgroundExecutor) { + InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, + ChannelPrimer channelPrimer, + ChannelPoolMetricsTracer outstandingRpcsMetricTracker, + ScheduledExecutorService backgroundExecutor, + DirectPathCompatibleTracer directPathCompatibleTracer) { return new BigtableTransportChannelProvider( - instantiatingGrpcChannelProvider, - channelPrimer, - outstandingRpcsMetricTracker, - backgroundExecutor); + instantiatingGrpcChannelProvider, + channelPrimer, + outstandingRpcsMetricTracker, + backgroundExecutor, + directPathCompatibleTracer); } -} +} \ No newline at end of file diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java index ea7cc70175..6234b4410c 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java @@ -18,11 +18,21 @@ import com.google.api.core.ApiFuture; import com.google.api.core.InternalApi; import com.google.bigtable.v2.PingAndWarmResponse; +import io.grpc.Channel; import io.grpc.ManagedChannel; @InternalApi("For internal use by google-cloud-java clients only") public interface ChannelPrimer { - void primeChannel(ManagedChannel channel); + @Deprecated + default void primeChannel(ManagedChannel channel) { + primeChannel((Channel) channel); + } - ApiFuture sendPrimeRequestsAsync(ManagedChannel channel); + void primeChannel(Channel channel); + + @Deprecated + default ApiFuture sendPrimeRequestsAsync(ManagedChannel channel) { + return sendPrimeRequestsAsync((Channel) channel); + } + ApiFuture sendPrimeRequestsAsync(Channel channel); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/DirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/DirectAccessChecker.java new file mode 100644 index 0000000000..5ab0109b25 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/DirectAccessChecker.java @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.gaxx.grpc; + +import com.google.api.core.InternalApi; +import io.grpc.Channel; + +@InternalApi +/* Evaluates whether a given channel supports Direct Access. */ +public interface DirectAccessChecker { + /// Performs a request on the provided channel to check for Direct Access eligibility. + boolean check(Channel channel ); +} \ No newline at end of file diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/UnaryDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/UnaryDirectAccessChecker.java new file mode 100644 index 0000000000..138dbd245c --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/UnaryDirectAccessChecker.java @@ -0,0 +1,58 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.gaxx.grpc; + +import com.google.api.core.InternalApi; +import com.google.bigtable.v2.PeerInfo; +import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor; +import io.grpc.Channel; +import io.grpc.ClientInterceptors; + +import java.util.Optional; + +/** + * Evaluates whether a given channel has Direct Access (DirectPath) routing + * by executing a RPC and inspecting the response headers. + */ +@InternalApi +public class UnaryDirectAccessChecker implements DirectAccessChecker { + + private final ChannelPrimer channelPrimer; + + private UnaryDirectAccessChecker(ChannelPrimer channelPrimer) { + this.channelPrimer = channelPrimer; + } + + public static UnaryDirectAccessChecker create(ChannelPrimer channelPrimer) { + return new UnaryDirectAccessChecker(channelPrimer); + } + + @Override + public boolean check(Channel channel) { + MetadataExtractorInterceptor interceptor = new MetadataExtractorInterceptor(); + Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor); + channelPrimer.primeChannel(interceptedChannel); + + // Extract the sideband data populated by the interceptor + MetadataExtractorInterceptor.SidebandData sidebandData = interceptor.getSidebandData(); + + return Optional.of(sidebandData) + .map(MetadataExtractorInterceptor.SidebandData::getPeerInfo) + .map(PeerInfo::getTransportType) + .map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) + .orElse(false); + } +} \ No newline at end of file From a6910b290aa2fed094c63c4cc31cb48b47832e00 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Tue, 10 Mar 2026 16:41:46 -0400 Subject: [PATCH 02/28] fix --- .../cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java index 85adcf5772..73c89e3778 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java @@ -29,10 +29,10 @@ import io.grpc.Channel; import io.grpc.ClientCall; import io.grpc.Deadline; -import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.Status; import io.grpc.auth.MoreCallCredentials; + import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; From 60bc8f4c9c3d0a4e7ff733866b42e0a477d51fb1 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Thu, 12 Mar 2026 03:15:32 -0400 Subject: [PATCH 03/28] fix --- .../data/v2/internal/csm/Metrics.java | 4 + .../data/v2/internal/csm/MetricsImpl.java | 11 + .../DefaultDirectPathCompatibleTracer.java | 42 ++ .../tracers/DirectPathCompatibleTracer.java | 39 ++ .../v2/stub/BigtableChannelFactory.java} | 12 +- .../data/v2/stub/BigtableClientContext.java | 3 +- .../data/v2/stub/DirectAccessChecker.java | 34 ++ .../v2/stub/EnhancedBigtableStubSettings.java | 8 +- .../v2/stub/MetadataExtractorInterceptor.java | 34 ++ .../v2/stub/UnaryDirectAccessChecker.java | 84 ++++ .../gaxx/grpc/BigtableChannelPool.java | 8 +- .../BigtableTransportChannelProvider.java | 446 +++++++++--------- .../gaxx/grpc/UnaryDirectAccessChecker.java | 58 --- .../v2/stub/UnaryDirectAccessCheckerTest.java | 134 ++++++ .../gaxx/grpc/BigtableChannelPoolTest.java | 4 +- 15 files changed, 625 insertions(+), 296 deletions(-) create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java rename google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/{gaxx/grpc/DirectAccessChecker.java => data/v2/stub/BigtableChannelFactory.java} (68%) create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java delete mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/UnaryDirectAccessChecker.java create mode 100644 google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java index 7df665c673..823458e0a4 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java @@ -18,6 +18,7 @@ import com.google.api.gax.tracing.ApiTracerFactory; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.ChannelPoolMetricsTracer; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; import io.grpc.ManagedChannelBuilder; import java.io.Closeable; import java.io.IOException; @@ -31,6 +32,9 @@ public interface Metrics extends Closeable { @Nullable ChannelPoolMetricsTracer getChannelPoolMetricsTracer(); + @Nullable + DirectPathCompatibleTracer getDirectPathCompatibleTracer(); + void start(); @Override diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java index f0efac7e96..5d4c8d0e58 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java @@ -31,6 +31,8 @@ import com.google.cloud.bigtable.data.v2.internal.csm.tracers.BuiltinMetricsTracerFactory; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.ChannelPoolMetricsTracer; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.CompositeTracerFactory; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DefaultDirectPathCompatibleTracer; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.Pacemaker; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -67,6 +69,7 @@ public class MetricsImpl implements Metrics, Closeable { @Nullable private final GrpcOpenTelemetry grpcOtel; @Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer; + @Nullable private final DirectPathCompatibleTracer directPathCompatibleTracer; @Nullable private final Pacemaker pacemaker; private final List> tasks = new ArrayList<>(); @@ -94,6 +97,7 @@ public MetricsImpl( this.internalRecorder = metricRegistry.newRecorderRegistry(internalOtel.getMeterProvider()); this.pacemaker = new Pacemaker(internalRecorder, clientInfo, "background"); this.channelPoolMetricsTracer = new ChannelPoolMetricsTracer(internalRecorder, clientInfo); + this.directPathCompatibleTracer = new DefaultDirectPathCompatibleTracer(clientInfo, internalRecorder); this.grpcOtel = GrpcOpenTelemetry.newBuilder() .sdk(internalOtel) @@ -109,6 +113,7 @@ public MetricsImpl( this.grpcOtel = null; this.pacemaker = null; this.channelPoolMetricsTracer = null; + this.directPathCompatibleTracer = null; } if (userOtel != null) { @@ -171,6 +176,12 @@ public ChannelPoolMetricsTracer getChannelPoolMetricsTracer() { return channelPoolMetricsTracer; } + @Nullable + @Override + public DirectPathCompatibleTracer getDirectPathCompatibleTracer() { + return directPathCompatibleTracer; + } + public static OpenTelemetrySdk createBuiltinOtel( MetricRegistry metricRegistry, ClientInfo clientInfo, diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java new file mode 100644 index 0000000000..041b226ab2 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.data.v2.internal.csm.tracers; + +import com.google.api.core.InternalApi; +import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry; +import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo; + +@InternalApi +public class DefaultDirectPathCompatibleTracer implements DirectPathCompatibleTracer { + private final ClientInfo clientInfo; + private final MetricRegistry.RecorderRegistry recorder; + + public DefaultDirectPathCompatibleTracer( + ClientInfo clientInfo, MetricRegistry.RecorderRegistry recorder) { + this.clientInfo = clientInfo; + this.recorder = recorder; + } + + @Override + public void recordSuccess(String ipPreference) { + recorder.dpCompatGuage.recordSuccess(clientInfo, ipPreference); + } + + @Override + public void recordFailure(String reason) { + recorder.dpCompatGuage.recordFailure(clientInfo, reason); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java new file mode 100644 index 0000000000..0a385494cf --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.data.v2.internal.csm.tracers; + +import com.google.api.core.InternalApi; + +/** + * Interface for recording DirectPath/DirectAccess eligibility metrics. + */ +@InternalApi +public interface DirectPathCompatibleTracer { + + /** + * Records that the environment is eligible and successfully connected via DirectPath. + * + * @param ipPreference The IP preference used (e.g., "ipv6"). + */ + void recordSuccess(String ipPreference); + + /** + * Records that the environment is not eligible or failed to connect via DirectPath. + * + * @param reason The reason for the failure (e.g., "routing_check_failed"). + */ + void recordFailure(String reason); +} \ No newline at end of file diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/DirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java similarity index 68% rename from google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/DirectAccessChecker.java rename to google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java index 5ab0109b25..a4ce11b58a 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/DirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.cloud.bigtable.gaxx.grpc; +package com.google.cloud.bigtable.data.v2.stub; import com.google.api.core.InternalApi; -import io.grpc.Channel; +import io.grpc.ManagedChannel; + +import java.io.IOException; @InternalApi -/* Evaluates whether a given channel supports Direct Access. */ -public interface DirectAccessChecker { - /// Performs a request on the provided channel to check for Direct Access eligibility. - boolean check(Channel channel ); +public interface BigtableChannelFactory { + ManagedChannel createSingleChannel() throws IOException; } \ No newline at end of file diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java index 47bb9ccff9..3bebe1844d 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java @@ -166,7 +166,8 @@ public static BigtableClientContext create( transportProvider.build(), channelPrimer, metrics.getChannelPoolMetricsTracer(), - backgroundExecutor); + backgroundExecutor, metrics.getDirectPathCompatibleTracer() + ); builder.setTransportChannelProvider(btTransportProvider); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java new file mode 100644 index 0000000000..281ab7b48f --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.data.v2.stub; + +import com.google.api.core.InternalApi; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; +import io.grpc.Channel; + +import javax.annotation.Nullable; + +@InternalApi +/* Evaluates whether a given channel supports Direct Access. */ +public interface DirectAccessChecker { + /** + * Evaluates if Direct Access is available by creating a test channel. + * + * @param channelFactory A factory to create the test channel + * @return true if the channel is eligible for Direct Access + */ + boolean check(BigtableChannelFactory channelFactory, @Nullable DirectPathCompatibleTracer tracer); +} \ No newline at end of file diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index 7ced90f8ef..47937216da 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -634,12 +634,16 @@ private Builder() { perOpSettings = new ClientOperationSettings.Builder(); + // Note: RouteLookup evaluates and returns directpath targets + // only if Traffic Director sends the request (with grpc as target type) + // For GFE/CFE, sending setDirectAccessRequested + // is fine as GFE/CFE sends with gslb target type featureFlags = FeatureFlags.newBuilder() .setReverseScans(true) .setLastScannedRowResponses(true) - .setDirectAccessRequested(DIRECT_PATH_ENABLED) - .setTrafficDirectorEnabled(DIRECT_PATH_ENABLED) + .setDirectAccessRequested(true) + .setTrafficDirectorEnabled(true) .setPeerInfo(true); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java index d38f164fc6..145e4064ea 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java @@ -29,10 +29,16 @@ import io.grpc.ClientInterceptors; import io.grpc.ForwardingClientCall; import io.grpc.ForwardingClientCallListener; +import io.grpc.Grpc; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; import io.grpc.alts.AltsContextUtil; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.time.Duration; import java.util.Base64; import java.util.regex.Matcher; @@ -85,6 +91,12 @@ public SidebandData getSidebandData() { } public static class SidebandData { + public enum IpProtocol { + IPV4, + IPV6, + UNKNOWN + } + private static final CallOptions.Key KEY = CallOptions.Key.create("bigtable-sideband"); @@ -105,6 +117,7 @@ public static SidebandData from(CallOptions callOptions) { @Nullable private volatile ResponseParams responseParams; @Nullable private volatile PeerInfo peerInfo; @Nullable private volatile Duration gfeTiming; + @Nullable private volatile IpProtocol ipProtocol; @Nullable public ResponseParams getResponseParams() { @@ -121,16 +134,23 @@ public Duration getGfeTiming() { return gfeTiming; } + @Nullable + public IpProtocol getIpProtocol() { + return ipProtocol; + } + private void reset() { responseParams = null; peerInfo = null; gfeTiming = null; + ipProtocol = IpProtocol.UNKNOWN; } void onResponseHeaders(Metadata md, Attributes attributes) { responseParams = extractResponseParams(md); gfeTiming = extractGfeLatency(md); peerInfo = extractPeerInfo(md, gfeTiming, attributes); + ipProtocol = extractIpProtocol(attributes); } void onClose(Status status, Metadata trailers) { @@ -139,6 +159,20 @@ void onClose(Status status, Metadata trailers) { } } + @Nullable + private static IpProtocol extractIpProtocol(Attributes attributes) { + SocketAddress remoteAddr = attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + if (remoteAddr instanceof InetSocketAddress) { + InetSocketAddress inetAddr = (InetSocketAddress) remoteAddr; + if (inetAddr.getAddress() instanceof Inet4Address) { + return IpProtocol.IPV4; + } else if (inetAddr.getAddress() instanceof Inet6Address) { + return IpProtocol.IPV6; + } + } + return IpProtocol.UNKNOWN; + } + @Nullable private static Duration extractGfeLatency(Metadata metadata) { String serverTiming = metadata.get(SERVER_TIMING_HEADER_KEY); diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java new file mode 100644 index 0000000000..edb842235f --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java @@ -0,0 +1,84 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.data.v2.stub; + +import com.google.api.core.InternalApi; +import com.google.bigtable.v2.PeerInfo; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; +import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer; +import io.grpc.Channel; +import io.grpc.ClientInterceptors; +import io.grpc.ManagedChannel; + +import javax.annotation.Nullable; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Evaluates whether a given channel has Direct Access (DirectPath) routing + * by executing a RPC and inspecting the response headers. + */ +@InternalApi +public class UnaryDirectAccessChecker implements DirectAccessChecker { + private static final Logger LOG = Logger.getLogger(UnaryDirectAccessChecker.class.getName()); + private final ChannelPrimer channelPrimer; + + private UnaryDirectAccessChecker(ChannelPrimer channelPrimer) { + this.channelPrimer = channelPrimer; + } + + public static UnaryDirectAccessChecker create(ChannelPrimer channelPrimer) { + return new UnaryDirectAccessChecker(channelPrimer); + } + + @Override + public boolean check(BigtableChannelFactory channelFactory, @Nullable DirectPathCompatibleTracer tracer) { + ManagedChannel channel = null; + try { + channel = channelFactory.createSingleChannel(); + MetadataExtractorInterceptor interceptor = new MetadataExtractorInterceptor(); + Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor); + channelPrimer.primeChannel(interceptedChannel); + + // Extract the sideband data populated by the interceptor + MetadataExtractorInterceptor.SidebandData sidebandData = interceptor.getSidebandData(); + + boolean isEligible = Optional.ofNullable(sidebandData) + .map(MetadataExtractorInterceptor.SidebandData::getPeerInfo) + .map(PeerInfo::getTransportType) + .map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) + .orElse(false); + + if (isEligible && tracer != null) { + String ipProtocolStr = Optional.ofNullable(sidebandData) + .map(MetadataExtractorInterceptor.SidebandData::getIpProtocol) + .map(String::valueOf) + .map(String::toLowerCase) + .orElse("unknown"); + tracer.recordSuccess(ipProtocolStr); + } + return isEligible; + } catch (Exception e) { + LOG.log(Level.FINE, "Failed to evaluate direct access eligibility.", e); + return false; + } finally { + if (channel != null) { + channel.shutdownNow(); + } + } + } +} \ No newline at end of file diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java index 188dc83ac0..0298b730e7 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java @@ -16,8 +16,8 @@ package com.google.cloud.bigtable.gaxx.grpc; import com.google.api.core.InternalApi; -import com.google.api.gax.grpc.ChannelFactory; import com.google.bigtable.v2.PeerInfo; +import com.google.cloud.bigtable.data.v2.stub.BigtableChannelFactory; import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor; import com.google.cloud.bigtable.gaxx.grpc.ChannelPoolHealthChecker.ProbeResult; import com.google.common.annotations.VisibleForTesting; @@ -67,7 +67,7 @@ public class BigtableChannelPool extends ManagedChannel implements BigtableChann private static final java.time.Duration REFRESH_PERIOD = java.time.Duration.ofMinutes(50); private final BigtableChannelPoolSettings settings; - private final ChannelFactory channelFactory; + private final BigtableChannelFactory channelFactory; private final ChannelPrimer channelPrimer; private final Object entryWriteLock = new Object(); @@ -81,7 +81,7 @@ public class BigtableChannelPool extends ManagedChannel implements BigtableChann public static BigtableChannelPool create( BigtableChannelPoolSettings settings, - ChannelFactory channelFactory, + BigtableChannelFactory channelFactory, ChannelPrimer channelPrimer, ScheduledExecutorService backgroundExecutor) throws IOException { @@ -98,7 +98,7 @@ public static BigtableChannelPool create( @VisibleForTesting BigtableChannelPool( BigtableChannelPoolSettings settings, - ChannelFactory channelFactory, + BigtableChannelFactory channelFactory, ChannelPrimer channelPrimer, ScheduledExecutorService executor) throws IOException { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java index facb28dcf8..2b3cb4022e 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java @@ -44,234 +44,234 @@ */ @InternalApi public final class BigtableTransportChannelProvider implements TransportChannelProvider { - private static final Logger LOG = - Logger.getLogger(BigtableTransportChannelProvider.class.getName()); - private final InstantiatingGrpcChannelProvider delegate; - private final ChannelPrimer channelPrimer; - @Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer; - @Nullable private final ScheduledExecutorService backgroundExecutor; - DirectPathCompatibleTracer directPathCompatibleTracer; - - private BigtableTransportChannelProvider( - InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, - ChannelPrimer channelPrimer, - @Nullable ChannelPoolMetricsTracer channelPoolMetricsTracer, - @Nullable ScheduledExecutorService backgroundExecutor, - DirectPathCompatibleTracer directPathCompatibleTracer) { - delegate = Preconditions.checkNotNull(instantiatingGrpcChannelProvider); - this.channelPrimer = channelPrimer; - this.channelPoolMetricsTracer = channelPoolMetricsTracer; - this.backgroundExecutor = backgroundExecutor; - this.directPathCompatibleTracer = directPathCompatibleTracer; - } - - @Override - public boolean shouldAutoClose() { - return delegate.shouldAutoClose(); - } - - @SuppressWarnings("deprecation") - @Override - public boolean needsExecutor() { - return delegate.needsExecutor(); - } - - @Override - public BigtableTransportChannelProvider withExecutor(ScheduledExecutorService executor) { - return withExecutor((Executor) executor); - } - - // This executor if set is for handling rpc callbacks so we can't use it as the background - // executor - @Override - public BigtableTransportChannelProvider withExecutor(Executor executor) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withExecutor(executor); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - backgroundExecutor, - directPathCompatibleTracer); - } - - @Override - public boolean needsBackgroundExecutor() { - return delegate.needsBackgroundExecutor(); - } - - @Override - public TransportChannelProvider withBackgroundExecutor(ScheduledExecutorService executor) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withBackgroundExecutor(executor); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - executor, - directPathCompatibleTracer); - } - - @Override - public boolean needsHeaders() { - return delegate.needsHeaders(); - } - - @Override - public BigtableTransportChannelProvider withHeaders(Map headers) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withHeaders(headers); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - backgroundExecutor, - directPathCompatibleTracer); - } - - @Override - public boolean needsEndpoint() { - return delegate.needsEndpoint(); - } - - @Override - public TransportChannelProvider withEndpoint(String endpoint) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withEndpoint(endpoint); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - backgroundExecutor, - directPathCompatibleTracer); - } - - @Deprecated - @Override - public boolean acceptsPoolSize() { - return delegate.acceptsPoolSize(); - } - - @Deprecated - @Override - public TransportChannelProvider withPoolSize(int size) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withPoolSize(size); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - backgroundExecutor, - directPathCompatibleTracer); - } - - // We need this for direct access checker. - - /** Expected to only be called once when BigtableClientContext is created */ - @Override - public TransportChannel getTransportChannel() throws IOException { - InstantiatingGrpcChannelProvider directAccessProvider = - EnhancedBigtableStubSettings.applyDirectAccessTraits(delegate.toBuilder()) - .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) - .build(); - - BigtableChannelFactory maybeDirectAccessChannelFactory = - () -> { - GrpcTransportChannel channel = - (GrpcTransportChannel) directAccessProvider.getTransportChannel(); - return (ManagedChannel) channel.getChannel(); - }; - - DirectAccessChecker directAccessChecker = UnaryDirectAccessChecker.create(channelPrimer); - boolean isDirectAccessEligible = false; - - try { - isDirectAccessEligible = - directAccessChecker.check(maybeDirectAccessChannelFactory, directPathCompatibleTracer); - } catch (Exception e) { - LOG.log(Level.FINE, "Client is not direct access eligible, using standard transport.", e); + private static final Logger LOG = + Logger.getLogger(BigtableTransportChannelProvider.class.getName()); + private final InstantiatingGrpcChannelProvider delegate; + private final ChannelPrimer channelPrimer; + @Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer; + @Nullable private final ScheduledExecutorService backgroundExecutor; + DirectPathCompatibleTracer directPathCompatibleTracer; + + private BigtableTransportChannelProvider( + InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, + ChannelPrimer channelPrimer, + @Nullable ChannelPoolMetricsTracer channelPoolMetricsTracer, + @Nullable ScheduledExecutorService backgroundExecutor, + DirectPathCompatibleTracer directPathCompatibleTracer) { + delegate = Preconditions.checkNotNull(instantiatingGrpcChannelProvider); + this.channelPrimer = channelPrimer; + this.channelPoolMetricsTracer = channelPoolMetricsTracer; + this.backgroundExecutor = backgroundExecutor; + this.directPathCompatibleTracer = directPathCompatibleTracer; } - InstantiatingGrpcChannelProvider selectedProvider; + @Override + public boolean shouldAutoClose() { + return delegate.shouldAutoClose(); + } + + @SuppressWarnings("deprecation") + @Override + public boolean needsExecutor() { + return delegate.needsExecutor(); + } + + @Override + public BigtableTransportChannelProvider withExecutor(ScheduledExecutorService executor) { + return withExecutor((Executor) executor); + } + + // This executor if set is for handling rpc callbacks so we can't use it as the background + // executor + @Override + public BigtableTransportChannelProvider withExecutor(Executor executor) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withExecutor(executor); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); + } + + @Override + public boolean needsBackgroundExecutor() { + return delegate.needsBackgroundExecutor(); + } + + @Override + public TransportChannelProvider withBackgroundExecutor(ScheduledExecutorService executor) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withBackgroundExecutor(executor); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + executor, + directPathCompatibleTracer); + } + + @Override + public boolean needsHeaders() { + return delegate.needsHeaders(); + } + + @Override + public BigtableTransportChannelProvider withHeaders(Map headers) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withHeaders(headers); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); + } + + @Override + public boolean needsEndpoint() { + return delegate.needsEndpoint(); + } + + @Override + public TransportChannelProvider withEndpoint(String endpoint) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withEndpoint(endpoint); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); + } + + @Deprecated + @Override + public boolean acceptsPoolSize() { + return delegate.acceptsPoolSize(); + } - if (isDirectAccessEligible) { - selectedProvider = directAccessProvider; - } else { - selectedProvider = delegate; + @Deprecated + @Override + public TransportChannelProvider withPoolSize(int size) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withPoolSize(size); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); } - // This provider's main purpose is to replace the default GAX ChannelPool - // with a custom BigtableChannelPool, reusing the delegate's configuration. - - // To create our pool, we need a factory for raw gRPC channels. - // We achieve this by configuring our delegate to not use its own pooling - // (by setting pool size to 1) and then calling getTransportChannel() on it. - InstantiatingGrpcChannelProvider singleChannelProvider = - selectedProvider.toBuilder() - .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) - .build(); - - BigtableChannelFactory channelFactory = - () -> { - try { - GrpcTransportChannel channel = - (GrpcTransportChannel) singleChannelProvider.getTransportChannel(); - return (ManagedChannel) channel.getChannel(); - } catch (IOException e) { - throw new java.io.UncheckedIOException(e); - } - }; - - BigtableChannelPoolSettings btPoolSettings = - BigtableChannelPoolSettings.copyFrom(delegate.getChannelPoolSettings()); - - BigtableChannelPool btChannelPool = - BigtableChannelPool.create( - btPoolSettings, channelFactory, channelPrimer, backgroundExecutor); - - if (channelPoolMetricsTracer != null) { - channelPoolMetricsTracer.registerChannelInsightsProvider(btChannelPool); - channelPoolMetricsTracer.registerLoadBalancingStrategy( - btPoolSettings.getLoadBalancingStrategy()); + // We need this for direct access checker. + + /** Expected to only be called once when BigtableClientContext is created */ + @Override + public TransportChannel getTransportChannel() throws IOException { + InstantiatingGrpcChannelProvider directAccessProvider = + EnhancedBigtableStubSettings.applyDirectAccessTraits(delegate.toBuilder()) + .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) + .build(); + + BigtableChannelFactory maybeDirectAccessChannelFactory = + () -> { + GrpcTransportChannel channel = + (GrpcTransportChannel) directAccessProvider.getTransportChannel(); + return (ManagedChannel) channel.getChannel(); + }; + + DirectAccessChecker directAccessChecker = UnaryDirectAccessChecker.create(channelPrimer); + boolean isDirectAccessEligible = false; + + try { + isDirectAccessEligible = + directAccessChecker.check(maybeDirectAccessChannelFactory, directPathCompatibleTracer); + } catch (Exception e) { + LOG.log(Level.FINE, "Client is not direct access eligible, using standard transport.", e); + } + + InstantiatingGrpcChannelProvider selectedProvider; + + if (isDirectAccessEligible) { + selectedProvider = directAccessProvider; + } else { + selectedProvider = delegate; + } + + // This provider's main purpose is to replace the default GAX ChannelPool + // with a custom BigtableChannelPool, reusing the delegate's configuration. + + // To create our pool, we need a factory for raw gRPC channels. + // We achieve this by configuring our delegate to not use its own pooling + // (by setting pool size to 1) and then calling getTransportChannel() on it. + InstantiatingGrpcChannelProvider singleChannelProvider = + selectedProvider.toBuilder() + .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) + .build(); + + BigtableChannelFactory channelFactory = + () -> { + try { + GrpcTransportChannel channel = + (GrpcTransportChannel) singleChannelProvider.getTransportChannel(); + return (ManagedChannel) channel.getChannel(); + } catch (IOException e) { + throw new java.io.UncheckedIOException(e); + } + }; + + BigtableChannelPoolSettings btPoolSettings = + BigtableChannelPoolSettings.copyFrom(delegate.getChannelPoolSettings()); + + BigtableChannelPool btChannelPool = + BigtableChannelPool.create( + btPoolSettings, channelFactory, channelPrimer, backgroundExecutor); + + if (channelPoolMetricsTracer != null) { + channelPoolMetricsTracer.registerChannelInsightsProvider(btChannelPool); + channelPoolMetricsTracer.registerLoadBalancingStrategy( + btPoolSettings.getLoadBalancingStrategy()); + } + + return GrpcTransportChannel.create(btChannelPool); } - return GrpcTransportChannel.create(btChannelPool); - } - - @Override - public String getTransportName() { - return "bigtable"; - } - - @Override - public boolean needsCredentials() { - return delegate.needsCredentials(); - } - - @Override - public TransportChannelProvider withCredentials(Credentials credentials) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withCredentials(credentials); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - backgroundExecutor, - directPathCompatibleTracer); - } - - /** Creates a BigtableTransportChannelProvider. */ - public static BigtableTransportChannelProvider create( - InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, - ChannelPrimer channelPrimer, - ChannelPoolMetricsTracer outstandingRpcsMetricTracker, - ScheduledExecutorService backgroundExecutor, - DirectPathCompatibleTracer directPathCompatibleTracer) { - return new BigtableTransportChannelProvider( - instantiatingGrpcChannelProvider, - channelPrimer, - outstandingRpcsMetricTracker, - backgroundExecutor, - directPathCompatibleTracer); - } + @Override + public String getTransportName() { + return "bigtable"; + } + + @Override + public boolean needsCredentials() { + return delegate.needsCredentials(); + } + + @Override + public TransportChannelProvider withCredentials(Credentials credentials) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withCredentials(credentials); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); + } + + /** Creates a BigtableTransportChannelProvider. */ + public static BigtableTransportChannelProvider create( + InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, + ChannelPrimer channelPrimer, + ChannelPoolMetricsTracer outstandingRpcsMetricTracker, + ScheduledExecutorService backgroundExecutor, + DirectPathCompatibleTracer directPathCompatibleTracer) { + return new BigtableTransportChannelProvider( + instantiatingGrpcChannelProvider, + channelPrimer, + outstandingRpcsMetricTracker, + backgroundExecutor, + directPathCompatibleTracer); + } } \ No newline at end of file diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/UnaryDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/UnaryDirectAccessChecker.java deleted file mode 100644 index 138dbd245c..0000000000 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/UnaryDirectAccessChecker.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.cloud.bigtable.gaxx.grpc; - -import com.google.api.core.InternalApi; -import com.google.bigtable.v2.PeerInfo; -import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor; -import io.grpc.Channel; -import io.grpc.ClientInterceptors; - -import java.util.Optional; - -/** - * Evaluates whether a given channel has Direct Access (DirectPath) routing - * by executing a RPC and inspecting the response headers. - */ -@InternalApi -public class UnaryDirectAccessChecker implements DirectAccessChecker { - - private final ChannelPrimer channelPrimer; - - private UnaryDirectAccessChecker(ChannelPrimer channelPrimer) { - this.channelPrimer = channelPrimer; - } - - public static UnaryDirectAccessChecker create(ChannelPrimer channelPrimer) { - return new UnaryDirectAccessChecker(channelPrimer); - } - - @Override - public boolean check(Channel channel) { - MetadataExtractorInterceptor interceptor = new MetadataExtractorInterceptor(); - Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor); - channelPrimer.primeChannel(interceptedChannel); - - // Extract the sideband data populated by the interceptor - MetadataExtractorInterceptor.SidebandData sidebandData = interceptor.getSidebandData(); - - return Optional.of(sidebandData) - .map(MetadataExtractorInterceptor.SidebandData::getPeerInfo) - .map(PeerInfo::getTransportType) - .map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) - .orElse(false); - } -} \ No newline at end of file diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java new file mode 100644 index 0000000000..5894565898 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java @@ -0,0 +1,134 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.data.v2.stub; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.google.bigtable.v2.PeerInfo; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; +import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer; +import io.grpc.Channel; +import io.grpc.ManagedChannel; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class UnaryDirectAccessCheckerTest { + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private ChannelPrimer mockChannelPrimer; + @Mock private BigtableChannelFactory mockChannelFactory; + @Mock private DirectPathCompatibleTracer mockTracer; + @Mock private ManagedChannel mockChannel; + @Mock private MetadataExtractorInterceptor.SidebandData mockSidebandData; + + private UnaryDirectAccessChecker checker; + + @Before + public void setUp() throws Exception { + checker = UnaryDirectAccessChecker.create(mockChannelPrimer); + when(mockChannelFactory.createSingleChannel()).thenReturn(mockChannel); + } + + @Test + public void testEligibleForDirectAccess() { + PeerInfo peerInfo = PeerInfo.newBuilder() + .setTransportType(PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) + .build(); + when(mockSidebandData.getPeerInfo()).thenReturn(peerInfo); + + when(mockSidebandData.getIpProtocol()).thenReturn(MetadataExtractorInterceptor.SidebandData.IpProtocol.IPV6); + + try (MockedConstruction ignored = + mockConstruction( + MetadataExtractorInterceptor.class, + (mock, context) -> { + when(mock.getSidebandData()).thenReturn(mockSidebandData); + })) { + + boolean isEligible = checker.check(mockChannelFactory, mockTracer); + + assertThat(isEligible).isFalse(); + verify(mockChannelPrimer).primeChannel(any(Channel.class)); + verify(mockTracer).recordSuccess("ipv6"); + verify(mockChannel).shutdownNow(); + } + } + + @Test + public void testNotEligibleProxiedRouting() { + // 1. Setup sideband data to simulate standard CloudPath routing + PeerInfo peerInfo = PeerInfo.newBuilder() + .setTransportType(PeerInfo.TransportType.TRANSPORT_TYPE_CLOUD_PATH) + .build(); + when(mockSidebandData.getPeerInfo()).thenReturn(peerInfo); + + try (MockedConstruction mocked = + mockConstruction( + MetadataExtractorInterceptor.class, + (mock, context) -> { + when(mock.getSidebandData()).thenReturn(mockSidebandData); + })) { + + boolean isEligible = checker.check(mockChannelFactory, mockTracer); + + assertThat(isEligible).isFalse(); + verifyNoInteractions(mockTracer); + verify(mockChannel).shutdownNow(); + } + } + + @Test + public void testMissingSidebandData() { + // Interceptor failed to capture anything (returns null) + try (MockedConstruction mocked = + mockConstruction( + MetadataExtractorInterceptor.class, + (mock, context) -> { + when(mock.getSidebandData()).thenReturn(null); + })) { + + boolean isEligible = checker.check(mockChannelFactory, mockTracer); + + assertThat(isEligible).isFalse(); + verifyNoInteractions(mockTracer); + verify(mockChannel).shutdownNow(); + } + } + + @Test + public void testExceptionSafetyAndCleanup() { + doThrow(new RuntimeException("Simulated primer failure")) + .when(mockChannelPrimer) + .primeChannel(any(Channel.class)); + + boolean isEligible = checker.check(mockChannelFactory, mockTracer); + + assertThat(isEligible).isFalse(); + verifyNoInteractions(mockTracer); + verify(mockChannel).shutdownNow(); + } +} \ No newline at end of file diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolTest.java index d1059c0362..1cd98a92c4 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolTest.java @@ -19,7 +19,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import com.google.api.gax.grpc.ChannelFactory; +import com.google.cloud.bigtable.data.v2.stub.BigtableChannelFactory; import com.google.common.collect.Iterables; import io.grpc.CallOptions; import io.grpc.ClientCall; @@ -48,7 +48,7 @@ public class BigtableChannelPoolTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); - @Mock private ChannelFactory mockChannelFactory; + @Mock private BigtableChannelFactory mockChannelFactory; @Mock private ChannelPrimer mockChannelPrimer; @Mock private ManagedChannel mockChannel; @Mock private ClientCall mockClientCall; From 547f461ecafddec4ea6b18b37aafdfcfb9042105 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Thu, 12 Mar 2026 12:02:27 -0400 Subject: [PATCH 04/28] fix --- .../cloud/bigtable/data/v2/stub/BigtableChannelFactory.java | 2 +- .../google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java | 2 +- .../cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java | 2 +- .../bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java index a4ce11b58a..80244b05ea 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java @@ -23,4 +23,4 @@ @InternalApi public interface BigtableChannelFactory { ManagedChannel createSingleChannel() throws IOException; -} \ No newline at end of file +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java index 281ab7b48f..b919d380bd 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java @@ -31,4 +31,4 @@ public interface DirectAccessChecker { * @return true if the channel is eligible for Direct Access */ boolean check(BigtableChannelFactory channelFactory, @Nullable DirectPathCompatibleTracer tracer); -} \ No newline at end of file +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java index edb842235f..0c8469ee58 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java @@ -81,4 +81,4 @@ public boolean check(BigtableChannelFactory channelFactory, @Nullable DirectPath } } } -} \ No newline at end of file +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java index 5894565898..4684c2a72f 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java @@ -131,4 +131,4 @@ public void testExceptionSafetyAndCleanup() { verifyNoInteractions(mockTracer); verify(mockChannel).shutdownNow(); } -} \ No newline at end of file +} From 30c17d562ceaf8a6b2a5aedff2dc99b0939f42eb Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Thu, 12 Mar 2026 12:13:39 -0400 Subject: [PATCH 05/28] fix --- .../data/v2/internal/csm/MetricsImpl.java | 3 +- .../DefaultDirectPathCompatibleTracer.java | 30 +-- .../tracers/DirectPathCompatibleTracer.java | 30 ++- .../data/v2/stub/BigtableChannelFactory.java | 3 +- .../data/v2/stub/BigtableChannelPrimer.java | 1 - .../data/v2/stub/BigtableClientContext.java | 4 +- .../data/v2/stub/DirectAccessChecker.java | 16 +- .../v2/stub/EnhancedBigtableStubSettings.java | 39 ++-- .../v2/stub/MetadataExtractorInterceptor.java | 1 - .../v2/stub/UnaryDirectAccessChecker.java | 90 ++++----- .../bigtable/gaxx/grpc/ChannelPrimer.java | 1 + .../v2/stub/UnaryDirectAccessCheckerTest.java | 179 +++++++++--------- 12 files changed, 198 insertions(+), 199 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java index 5d4c8d0e58..5c384a55fb 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java @@ -97,7 +97,8 @@ public MetricsImpl( this.internalRecorder = metricRegistry.newRecorderRegistry(internalOtel.getMeterProvider()); this.pacemaker = new Pacemaker(internalRecorder, clientInfo, "background"); this.channelPoolMetricsTracer = new ChannelPoolMetricsTracer(internalRecorder, clientInfo); - this.directPathCompatibleTracer = new DefaultDirectPathCompatibleTracer(clientInfo, internalRecorder); + this.directPathCompatibleTracer = + new DefaultDirectPathCompatibleTracer(clientInfo, internalRecorder); this.grpcOtel = GrpcOpenTelemetry.newBuilder() .sdk(internalOtel) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java index 041b226ab2..465e147868 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java @@ -21,22 +21,22 @@ @InternalApi public class DefaultDirectPathCompatibleTracer implements DirectPathCompatibleTracer { - private final ClientInfo clientInfo; - private final MetricRegistry.RecorderRegistry recorder; + private final ClientInfo clientInfo; + private final MetricRegistry.RecorderRegistry recorder; - public DefaultDirectPathCompatibleTracer( - ClientInfo clientInfo, MetricRegistry.RecorderRegistry recorder) { - this.clientInfo = clientInfo; - this.recorder = recorder; - } + public DefaultDirectPathCompatibleTracer( + ClientInfo clientInfo, MetricRegistry.RecorderRegistry recorder) { + this.clientInfo = clientInfo; + this.recorder = recorder; + } - @Override - public void recordSuccess(String ipPreference) { - recorder.dpCompatGuage.recordSuccess(clientInfo, ipPreference); - } + @Override + public void recordSuccess(String ipPreference) { + recorder.dpCompatGuage.recordSuccess(clientInfo, ipPreference); + } - @Override - public void recordFailure(String reason) { - recorder.dpCompatGuage.recordFailure(clientInfo, reason); - } + @Override + public void recordFailure(String reason) { + recorder.dpCompatGuage.recordFailure(clientInfo, reason); + } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java index 0a385494cf..4776785dc2 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java @@ -17,23 +17,21 @@ import com.google.api.core.InternalApi; -/** - * Interface for recording DirectPath/DirectAccess eligibility metrics. - */ +/** Interface for recording DirectPath/DirectAccess eligibility metrics. */ @InternalApi public interface DirectPathCompatibleTracer { - /** - * Records that the environment is eligible and successfully connected via DirectPath. - * - * @param ipPreference The IP preference used (e.g., "ipv6"). - */ - void recordSuccess(String ipPreference); + /** + * Records that the environment is eligible and successfully connected via DirectPath. + * + * @param ipPreference The IP preference used (e.g., "ipv6"). + */ + void recordSuccess(String ipPreference); - /** - * Records that the environment is not eligible or failed to connect via DirectPath. - * - * @param reason The reason for the failure (e.g., "routing_check_failed"). - */ - void recordFailure(String reason); -} \ No newline at end of file + /** + * Records that the environment is not eligible or failed to connect via DirectPath. + * + * @param reason The reason for the failure (e.g., "routing_check_failed"). + */ + void recordFailure(String reason); +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java index 80244b05ea..ce814e3073 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java @@ -17,10 +17,9 @@ import com.google.api.core.InternalApi; import io.grpc.ManagedChannel; - import java.io.IOException; @InternalApi public interface BigtableChannelFactory { - ManagedChannel createSingleChannel() throws IOException; + ManagedChannel createSingleChannel() throws IOException; } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java index 73c89e3778..bcec4c5403 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelPrimer.java @@ -32,7 +32,6 @@ import io.grpc.Metadata; import io.grpc.Status; import io.grpc.auth.MoreCallCredentials; - import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java index 3bebe1844d..2680558995 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java @@ -166,8 +166,8 @@ public static BigtableClientContext create( transportProvider.build(), channelPrimer, metrics.getChannelPoolMetricsTracer(), - backgroundExecutor, metrics.getDirectPathCompatibleTracer() - ); + backgroundExecutor, + metrics.getDirectPathCompatibleTracer()); builder.setTransportChannelProvider(btTransportProvider); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java index b919d380bd..9c5d66c559 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java @@ -17,18 +17,16 @@ import com.google.api.core.InternalApi; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; -import io.grpc.Channel; - import javax.annotation.Nullable; @InternalApi /* Evaluates whether a given channel supports Direct Access. */ public interface DirectAccessChecker { - /** - * Evaluates if Direct Access is available by creating a test channel. - * - * @param channelFactory A factory to create the test channel - * @return true if the channel is eligible for Direct Access - */ - boolean check(BigtableChannelFactory channelFactory, @Nullable DirectPathCompatibleTracer tracer); + /** + * Evaluates if Direct Access is available by creating a test channel. + * + * @param channelFactory A factory to create the test channel + * @return true if the channel is eligible for Direct Access + */ + boolean check(BigtableChannelFactory channelFactory, @Nullable DirectPathCompatibleTracer tracer); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index 47937216da..88904a59c0 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -244,21 +244,21 @@ public ClientOperationSettings getPerOpSettings() { /** Applies common pool, message size, and keep-alive settings to the provided builder. */ private static InstantiatingGrpcChannelProvider.Builder commonTraits( - InstantiatingGrpcChannelProvider.Builder builder) { + InstantiatingGrpcChannelProvider.Builder builder) { return builder - .setChannelPoolSettings( - ChannelPoolSettings.builder() - .setInitialChannelCount(10) - .setMinRpcsPerChannel(1) - // Keep it conservative as we scale the channel size every 1min - // and delta is 2 channels. - .setMaxRpcsPerChannel(25) - .setPreemptiveRefreshEnabled(true) - .build()) - .setMaxInboundMessageSize(MAX_MESSAGE_SIZE) - .setKeepAliveTime(Duration.ofSeconds(30)) // sends ping in this interval - .setKeepAliveTimeout( - Duration.ofSeconds(10)); // wait this long before considering the connection dead + .setChannelPoolSettings( + ChannelPoolSettings.builder() + .setInitialChannelCount(10) + .setMinRpcsPerChannel(1) + // Keep it conservative as we scale the channel size every 1min + // and delta is 2 channels. + .setMaxRpcsPerChannel(25) + .setPreemptiveRefreshEnabled(true) + .build()) + .setMaxInboundMessageSize(MAX_MESSAGE_SIZE) + .setKeepAliveTime(Duration.ofSeconds(30)) // sends ping in this interval + .setKeepAliveTimeout( + Duration.ofSeconds(10)); // wait this long before considering the connection dead } /** Returns a builder for the default ChannelProvider for this service. */ @@ -285,22 +285,21 @@ public static InstantiatingGrpcChannelProvider.Builder defaultGrpcTransportProvi /** Applies Direct Access traits (DirectPath & ALTS) to an existing builder. */ public static InstantiatingGrpcChannelProvider.Builder applyDirectAccessTraits( - InstantiatingGrpcChannelProvider.Builder builder) { + InstantiatingGrpcChannelProvider.Builder builder) { builder - .setAttemptDirectPathXds() - .setAttemptDirectPath(true) - .setAllowNonDefaultServiceAccount(true); + .setAttemptDirectPathXds() + .setAttemptDirectPath(true) + .setAllowNonDefaultServiceAccount(true); if (!DIRECT_PATH_BOUND_TOKEN_DISABLED) { builder.setAllowHardBoundTokenTypes( - Collections.singletonList(InstantiatingGrpcChannelProvider.HardBoundTokenTypes.ALTS)); + Collections.singletonList(InstantiatingGrpcChannelProvider.HardBoundTokenTypes.ALTS)); } return builder; } - @SuppressWarnings("WeakerAccess") public static TransportChannelProvider defaultTransportChannelProvider() { return defaultGrpcTransportProviderBuilder().build(); diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java index 145e4064ea..f2d3a1502f 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java @@ -34,7 +34,6 @@ import io.grpc.MethodDescriptor; import io.grpc.Status; import io.grpc.alts.AltsContextUtil; - import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetSocketAddress; diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java index 0c8469ee58..9549fbe403 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java @@ -22,63 +22,65 @@ import io.grpc.Channel; import io.grpc.ClientInterceptors; import io.grpc.ManagedChannel; - -import javax.annotation.Nullable; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nullable; /** - * Evaluates whether a given channel has Direct Access (DirectPath) routing - * by executing a RPC and inspecting the response headers. + * Evaluates whether a given channel has Direct Access (DirectPath) routing by executing a RPC and + * inspecting the response headers. */ @InternalApi public class UnaryDirectAccessChecker implements DirectAccessChecker { - private static final Logger LOG = Logger.getLogger(UnaryDirectAccessChecker.class.getName()); - private final ChannelPrimer channelPrimer; + private static final Logger LOG = Logger.getLogger(UnaryDirectAccessChecker.class.getName()); + private final ChannelPrimer channelPrimer; - private UnaryDirectAccessChecker(ChannelPrimer channelPrimer) { - this.channelPrimer = channelPrimer; - } + private UnaryDirectAccessChecker(ChannelPrimer channelPrimer) { + this.channelPrimer = channelPrimer; + } - public static UnaryDirectAccessChecker create(ChannelPrimer channelPrimer) { - return new UnaryDirectAccessChecker(channelPrimer); - } + public static UnaryDirectAccessChecker create(ChannelPrimer channelPrimer) { + return new UnaryDirectAccessChecker(channelPrimer); + } - @Override - public boolean check(BigtableChannelFactory channelFactory, @Nullable DirectPathCompatibleTracer tracer) { - ManagedChannel channel = null; - try { - channel = channelFactory.createSingleChannel(); - MetadataExtractorInterceptor interceptor = new MetadataExtractorInterceptor(); - Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor); - channelPrimer.primeChannel(interceptedChannel); + @Override + public boolean check( + BigtableChannelFactory channelFactory, @Nullable DirectPathCompatibleTracer tracer) { + ManagedChannel channel = null; + try { + channel = channelFactory.createSingleChannel(); + MetadataExtractorInterceptor interceptor = new MetadataExtractorInterceptor(); + Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor); + channelPrimer.primeChannel(interceptedChannel); - // Extract the sideband data populated by the interceptor - MetadataExtractorInterceptor.SidebandData sidebandData = interceptor.getSidebandData(); + // Extract the sideband data populated by the interceptor + MetadataExtractorInterceptor.SidebandData sidebandData = interceptor.getSidebandData(); - boolean isEligible = Optional.ofNullable(sidebandData) - .map(MetadataExtractorInterceptor.SidebandData::getPeerInfo) - .map(PeerInfo::getTransportType) - .map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) - .orElse(false); + boolean isEligible = + Optional.ofNullable(sidebandData) + .map(MetadataExtractorInterceptor.SidebandData::getPeerInfo) + .map(PeerInfo::getTransportType) + .map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) + .orElse(false); - if (isEligible && tracer != null) { - String ipProtocolStr = Optional.ofNullable(sidebandData) - .map(MetadataExtractorInterceptor.SidebandData::getIpProtocol) - .map(String::valueOf) - .map(String::toLowerCase) - .orElse("unknown"); - tracer.recordSuccess(ipProtocolStr); - } - return isEligible; - } catch (Exception e) { - LOG.log(Level.FINE, "Failed to evaluate direct access eligibility.", e); - return false; - } finally { - if (channel != null) { - channel.shutdownNow(); - } - } + if (isEligible && tracer != null) { + String ipProtocolStr = + Optional.ofNullable(sidebandData) + .map(MetadataExtractorInterceptor.SidebandData::getIpProtocol) + .map(String::valueOf) + .map(String::toLowerCase) + .orElse("unknown"); + tracer.recordSuccess(ipProtocolStr); + } + return isEligible; + } catch (Exception e) { + LOG.log(Level.FINE, "Failed to evaluate direct access eligibility.", e); + return false; + } finally { + if (channel != null) { + channel.shutdownNow(); + } } + } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java index 6234b4410c..b9fb28bcb6 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java @@ -34,5 +34,6 @@ default void primeChannel(ManagedChannel channel) { default ApiFuture sendPrimeRequestsAsync(ManagedChannel channel) { return sendPrimeRequestsAsync((Channel) channel); } + ApiFuture sendPrimeRequestsAsync(Channel channel); } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java index 4684c2a72f..275c6313ce 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java @@ -37,98 +37,101 @@ @RunWith(JUnit4.class) public class UnaryDirectAccessCheckerTest { - @Rule public final MockitoRule mockito = MockitoJUnit.rule(); - - @Mock private ChannelPrimer mockChannelPrimer; - @Mock private BigtableChannelFactory mockChannelFactory; - @Mock private DirectPathCompatibleTracer mockTracer; - @Mock private ManagedChannel mockChannel; - @Mock private MetadataExtractorInterceptor.SidebandData mockSidebandData; - - private UnaryDirectAccessChecker checker; - - @Before - public void setUp() throws Exception { - checker = UnaryDirectAccessChecker.create(mockChannelPrimer); - when(mockChannelFactory.createSingleChannel()).thenReturn(mockChannel); - } - - @Test - public void testEligibleForDirectAccess() { - PeerInfo peerInfo = PeerInfo.newBuilder() - .setTransportType(PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) - .build(); - when(mockSidebandData.getPeerInfo()).thenReturn(peerInfo); - - when(mockSidebandData.getIpProtocol()).thenReturn(MetadataExtractorInterceptor.SidebandData.IpProtocol.IPV6); - - try (MockedConstruction ignored = - mockConstruction( - MetadataExtractorInterceptor.class, - (mock, context) -> { - when(mock.getSidebandData()).thenReturn(mockSidebandData); - })) { - - boolean isEligible = checker.check(mockChannelFactory, mockTracer); - - assertThat(isEligible).isFalse(); - verify(mockChannelPrimer).primeChannel(any(Channel.class)); - verify(mockTracer).recordSuccess("ipv6"); - verify(mockChannel).shutdownNow(); - } + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private ChannelPrimer mockChannelPrimer; + @Mock private BigtableChannelFactory mockChannelFactory; + @Mock private DirectPathCompatibleTracer mockTracer; + @Mock private ManagedChannel mockChannel; + @Mock private MetadataExtractorInterceptor.SidebandData mockSidebandData; + + private UnaryDirectAccessChecker checker; + + @Before + public void setUp() throws Exception { + checker = UnaryDirectAccessChecker.create(mockChannelPrimer); + when(mockChannelFactory.createSingleChannel()).thenReturn(mockChannel); + } + + @Test + public void testEligibleForDirectAccess() { + PeerInfo peerInfo = + PeerInfo.newBuilder() + .setTransportType(PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) + .build(); + when(mockSidebandData.getPeerInfo()).thenReturn(peerInfo); + + when(mockSidebandData.getIpProtocol()) + .thenReturn(MetadataExtractorInterceptor.SidebandData.IpProtocol.IPV6); + + try (MockedConstruction ignored = + mockConstruction( + MetadataExtractorInterceptor.class, + (mock, context) -> { + when(mock.getSidebandData()).thenReturn(mockSidebandData); + })) { + + boolean isEligible = checker.check(mockChannelFactory, mockTracer); + + assertThat(isEligible).isFalse(); + verify(mockChannelPrimer).primeChannel(any(Channel.class)); + verify(mockTracer).recordSuccess("ipv6"); + verify(mockChannel).shutdownNow(); } - - @Test - public void testNotEligibleProxiedRouting() { - // 1. Setup sideband data to simulate standard CloudPath routing - PeerInfo peerInfo = PeerInfo.newBuilder() - .setTransportType(PeerInfo.TransportType.TRANSPORT_TYPE_CLOUD_PATH) - .build(); - when(mockSidebandData.getPeerInfo()).thenReturn(peerInfo); - - try (MockedConstruction mocked = - mockConstruction( - MetadataExtractorInterceptor.class, - (mock, context) -> { - when(mock.getSidebandData()).thenReturn(mockSidebandData); - })) { - - boolean isEligible = checker.check(mockChannelFactory, mockTracer); - - assertThat(isEligible).isFalse(); - verifyNoInteractions(mockTracer); - verify(mockChannel).shutdownNow(); - } + } + + @Test + public void testNotEligibleProxiedRouting() { + // 1. Setup sideband data to simulate standard CloudPath routing + PeerInfo peerInfo = + PeerInfo.newBuilder() + .setTransportType(PeerInfo.TransportType.TRANSPORT_TYPE_CLOUD_PATH) + .build(); + when(mockSidebandData.getPeerInfo()).thenReturn(peerInfo); + + try (MockedConstruction mocked = + mockConstruction( + MetadataExtractorInterceptor.class, + (mock, context) -> { + when(mock.getSidebandData()).thenReturn(mockSidebandData); + })) { + + boolean isEligible = checker.check(mockChannelFactory, mockTracer); + + assertThat(isEligible).isFalse(); + verifyNoInteractions(mockTracer); + verify(mockChannel).shutdownNow(); } - - @Test - public void testMissingSidebandData() { - // Interceptor failed to capture anything (returns null) - try (MockedConstruction mocked = - mockConstruction( - MetadataExtractorInterceptor.class, - (mock, context) -> { - when(mock.getSidebandData()).thenReturn(null); - })) { - - boolean isEligible = checker.check(mockChannelFactory, mockTracer); - - assertThat(isEligible).isFalse(); - verifyNoInteractions(mockTracer); - verify(mockChannel).shutdownNow(); - } + } + + @Test + public void testMissingSidebandData() { + // Interceptor failed to capture anything (returns null) + try (MockedConstruction mocked = + mockConstruction( + MetadataExtractorInterceptor.class, + (mock, context) -> { + when(mock.getSidebandData()).thenReturn(null); + })) { + + boolean isEligible = checker.check(mockChannelFactory, mockTracer); + + assertThat(isEligible).isFalse(); + verifyNoInteractions(mockTracer); + verify(mockChannel).shutdownNow(); } + } - @Test - public void testExceptionSafetyAndCleanup() { - doThrow(new RuntimeException("Simulated primer failure")) - .when(mockChannelPrimer) - .primeChannel(any(Channel.class)); + @Test + public void testExceptionSafetyAndCleanup() { + doThrow(new RuntimeException("Simulated primer failure")) + .when(mockChannelPrimer) + .primeChannel(any(Channel.class)); - boolean isEligible = checker.check(mockChannelFactory, mockTracer); + boolean isEligible = checker.check(mockChannelFactory, mockTracer); - assertThat(isEligible).isFalse(); - verifyNoInteractions(mockTracer); - verify(mockChannel).shutdownNow(); - } + assertThat(isEligible).isFalse(); + verifyNoInteractions(mockTracer); + verify(mockChannel).shutdownNow(); + } } From f69837e5407ff318e8a41b9d45e9cfd8b0490181 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Sat, 21 Mar 2026 00:25:43 -0400 Subject: [PATCH 06/28] address review comments --- .../data/v2/internal/csm/Metrics.java | 1 - .../data/v2/internal/csm/MetricsImpl.java | 3 +- .../NoopDirectPathCompatibleTracer.java | 37 ++ .../dp/ClassicDirectAccessChecker.java} | 38 +- .../dp}/DirectAccessChecker.java | 8 +- ...tory.java => BigtableChannelSupplier.java} | 6 +- .../v2/stub/EnhancedBigtableStubSettings.java | 4 +- .../gaxx/grpc/BigtableChannelPool.java | 21 +- .../BigtableTransportChannelProvider.java | 459 +++++++++--------- .../v2/BigtableDataClientFactoryTest.java | 7 +- .../dp/ClassicDirectAccessCheckerTest.java} | 81 ++-- .../v2/stub/EnhancedBigtableStubTest.java | 3 +- .../gaxx/grpc/BigtableChannelPoolTest.java | 6 +- 13 files changed, 355 insertions(+), 319 deletions(-) create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java rename google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/{stub/UnaryDirectAccessChecker.java => internal/dp/ClassicDirectAccessChecker.java} (67%) rename google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/{stub => internal/dp}/DirectAccessChecker.java (78%) rename google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/{BigtableChannelFactory.java => BigtableChannelSupplier.java} (84%) rename google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/{stub/UnaryDirectAccessCheckerTest.java => internal/dp/ClassicDirectAccessCheckerTest.java} (58%) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java index 823458e0a4..6e30d3dd2b 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java @@ -32,7 +32,6 @@ public interface Metrics extends Closeable { @Nullable ChannelPoolMetricsTracer getChannelPoolMetricsTracer(); - @Nullable DirectPathCompatibleTracer getDirectPathCompatibleTracer(); void start(); diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java index 5c384a55fb..69391b9796 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java @@ -33,6 +33,7 @@ import com.google.cloud.bigtable.data.v2.internal.csm.tracers.CompositeTracerFactory; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DefaultDirectPathCompatibleTracer; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.NoopDirectPathCompatibleTracer; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.Pacemaker; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -114,7 +115,7 @@ public MetricsImpl( this.grpcOtel = null; this.pacemaker = null; this.channelPoolMetricsTracer = null; - this.directPathCompatibleTracer = null; + this.directPathCompatibleTracer = NoopDirectPathCompatibleTracer.INSTANCE; } if (userOtel != null) { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java new file mode 100644 index 0000000000..bcfb8a48f7 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.data.v2.internal.csm.tracers; + +import com.google.api.core.InternalApi; + +@InternalApi +public class NoopDirectPathCompatibleTracer implements DirectPathCompatibleTracer { + + public static final NoopDirectPathCompatibleTracer INSTANCE = + new NoopDirectPathCompatibleTracer(); + + private NoopDirectPathCompatibleTracer() {} + + @Override + public void recordSuccess(String ipPreference) { + // No-op + } + + @Override + public void recordFailure(String reason) { + // No-op + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java similarity index 67% rename from google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java rename to google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java index 9549fbe403..cfd14741cc 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java @@ -13,16 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.cloud.bigtable.data.v2.stub; +package com.google.cloud.bigtable.data.v2.internal.dp; import com.google.api.core.InternalApi; import com.google.bigtable.v2.PeerInfo; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; +import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor; import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer; +import com.google.common.annotations.VisibleForTesting; import io.grpc.Channel; import io.grpc.ClientInterceptors; import io.grpc.ManagedChannel; import java.util.Optional; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -32,25 +35,30 @@ * inspecting the response headers. */ @InternalApi -public class UnaryDirectAccessChecker implements DirectAccessChecker { - private static final Logger LOG = Logger.getLogger(UnaryDirectAccessChecker.class.getName()); +public class ClassicDirectAccessChecker implements DirectAccessChecker { + private static final Logger LOG = Logger.getLogger(ClassicDirectAccessChecker.class.getName()); private final ChannelPrimer channelPrimer; - private UnaryDirectAccessChecker(ChannelPrimer channelPrimer) { + private ClassicDirectAccessChecker(ChannelPrimer channelPrimer) { this.channelPrimer = channelPrimer; } - public static UnaryDirectAccessChecker create(ChannelPrimer channelPrimer) { - return new UnaryDirectAccessChecker(channelPrimer); + public static ClassicDirectAccessChecker create(ChannelPrimer channelPrimer) { + return new ClassicDirectAccessChecker(channelPrimer); + } + + @VisibleForTesting + MetadataExtractorInterceptor createInterceptor() { + return new MetadataExtractorInterceptor(); } @Override public boolean check( - BigtableChannelFactory channelFactory, @Nullable DirectPathCompatibleTracer tracer) { + Supplier channelSupplier, @Nullable DirectPathCompatibleTracer tracer) { ManagedChannel channel = null; try { - channel = channelFactory.createSingleChannel(); - MetadataExtractorInterceptor interceptor = new MetadataExtractorInterceptor(); + channel = channelSupplier.get(); + MetadataExtractorInterceptor interceptor = createInterceptor(); Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor); channelPrimer.primeChannel(interceptedChannel); @@ -64,16 +72,16 @@ public boolean check( .map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) .orElse(false); - if (isEligible && tracer != null) { + if (isEligible) { String ipProtocolStr = - Optional.ofNullable(sidebandData) - .map(MetadataExtractorInterceptor.SidebandData::getIpProtocol) - .map(String::valueOf) - .map(String::toLowerCase) - .orElse("unknown"); + sidebandData.getIpProtocol() != null + ? sidebandData.getIpProtocol().toString().toLowerCase() + : "unknown"; tracer.recordSuccess(ipProtocolStr); } + return isEligible; + } catch (Exception e) { LOG.log(Level.FINE, "Failed to evaluate direct access eligibility.", e); return false; diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java similarity index 78% rename from google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java rename to google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java index 9c5d66c559..beab21e8d4 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/DirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.cloud.bigtable.data.v2.stub; +package com.google.cloud.bigtable.data.v2.internal.dp; import com.google.api.core.InternalApi; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; +import io.grpc.ManagedChannel; +import java.util.function.Supplier; import javax.annotation.Nullable; @InternalApi @@ -25,8 +27,8 @@ public interface DirectAccessChecker { /** * Evaluates if Direct Access is available by creating a test channel. * - * @param channelFactory A factory to create the test channel + * @param supplier A supplier to create maybe direct access channel * @return true if the channel is eligible for Direct Access */ - boolean check(BigtableChannelFactory channelFactory, @Nullable DirectPathCompatibleTracer tracer); + boolean check(Supplier supplier, @Nullable DirectPathCompatibleTracer tracer); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelSupplier.java similarity index 84% rename from google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java rename to google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelSupplier.java index ce814e3073..24aa062e14 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelFactory.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelSupplier.java @@ -17,9 +17,7 @@ import com.google.api.core.InternalApi; import io.grpc.ManagedChannel; -import java.io.IOException; +import java.util.function.Supplier; @InternalApi -public interface BigtableChannelFactory { - ManagedChannel createSingleChannel() throws IOException; -} +public interface BigtableChannelSupplier extends Supplier {} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index 88904a59c0..76c3e49b51 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -641,8 +641,8 @@ private Builder() { FeatureFlags.newBuilder() .setReverseScans(true) .setLastScannedRowResponses(true) - .setDirectAccessRequested(true) - .setTrafficDirectorEnabled(true) + .setDirectAccessRequested(DIRECT_PATH_ENABLED) + .setTrafficDirectorEnabled(DIRECT_PATH_ENABLED) .setPeerInfo(true); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java index 0298b730e7..4a127a5502 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java @@ -17,7 +17,6 @@ import com.google.api.core.InternalApi; import com.google.bigtable.v2.PeerInfo; -import com.google.cloud.bigtable.data.v2.stub.BigtableChannelFactory; import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor; import com.google.cloud.bigtable.gaxx.grpc.ChannelPoolHealthChecker.ProbeResult; import com.google.common.annotations.VisibleForTesting; @@ -67,7 +66,7 @@ public class BigtableChannelPool extends ManagedChannel implements BigtableChann private static final java.time.Duration REFRESH_PERIOD = java.time.Duration.ofMinutes(50); private final BigtableChannelPoolSettings settings; - private final BigtableChannelFactory channelFactory; + private final Supplier channelSupplier; private final ChannelPrimer channelPrimer; private final Object entryWriteLock = new Object(); @@ -81,11 +80,11 @@ public class BigtableChannelPool extends ManagedChannel implements BigtableChann public static BigtableChannelPool create( BigtableChannelPoolSettings settings, - BigtableChannelFactory channelFactory, + Supplier channelSupplier, ChannelPrimer channelPrimer, ScheduledExecutorService backgroundExecutor) throws IOException { - return new BigtableChannelPool(settings, channelFactory, channelPrimer, backgroundExecutor); + return new BigtableChannelPool(settings, channelSupplier, channelPrimer, backgroundExecutor); } /** @@ -98,12 +97,12 @@ public static BigtableChannelPool create( @VisibleForTesting BigtableChannelPool( BigtableChannelPoolSettings settings, - BigtableChannelFactory channelFactory, + Supplier channelSupplier, ChannelPrimer channelPrimer, ScheduledExecutorService executor) throws IOException { this.settings = settings; - this.channelFactory = channelFactory; + this.channelSupplier = channelSupplier; this.channelPrimer = channelPrimer; Clock systemClock = Clock.systemUTC(); ChannelPoolHealthChecker channelPoolHealthChecker = @@ -113,7 +112,7 @@ public static BigtableChannelPool create( ImmutableList.Builder initialListBuilder = ImmutableList.builder(); for (int i = 0; i < settings.getInitialChannelCount(); i++) { - ManagedChannel newChannel = channelFactory.createSingleChannel(); + ManagedChannel newChannel = channelSupplier.get(); channelPrimer.primeChannel(newChannel); initialListBuilder.add(new Entry(newChannel)); } @@ -419,10 +418,10 @@ private void expand(int desiredSize) { for (int i = 0; i < desiredSize - localEntries.size(); i++) { try { - ManagedChannel newChannel = channelFactory.createSingleChannel(); + ManagedChannel newChannel = channelSupplier.get(); this.channelPrimer.primeChannel(newChannel); newEntries.add(new Entry(newChannel)); - } catch (IOException e) { + } catch (Exception e) { LOG.log(Level.WARNING, "Failed to add channel", e); } } @@ -459,10 +458,10 @@ void refresh() { for (int i = 0; i < newEntries.size(); i++) { try { - ManagedChannel newChannel = channelFactory.createSingleChannel(); + ManagedChannel newChannel = channelSupplier.get(); this.channelPrimer.primeChannel(newChannel); newEntries.set(i, new Entry(newChannel)); - } catch (IOException e) { + } catch (Exception e) { LOG.log(Level.WARNING, "Failed to refresh channel, leaving old channel", e); } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java index 2b3cb4022e..8f3342bba4 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java @@ -24,16 +24,17 @@ import com.google.auth.Credentials; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.ChannelPoolMetricsTracer; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; -import com.google.cloud.bigtable.data.v2.stub.BigtableChannelFactory; -import com.google.cloud.bigtable.data.v2.stub.DirectAccessChecker; +import com.google.cloud.bigtable.data.v2.internal.dp.ClassicDirectAccessChecker; +import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessChecker; +import com.google.cloud.bigtable.data.v2.stub.BigtableChannelSupplier; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStubSettings; -import com.google.cloud.bigtable.data.v2.stub.UnaryDirectAccessChecker; import com.google.common.base.Preconditions; import io.grpc.ManagedChannel; import java.io.IOException; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -44,234 +45,238 @@ */ @InternalApi public final class BigtableTransportChannelProvider implements TransportChannelProvider { - private static final Logger LOG = - Logger.getLogger(BigtableTransportChannelProvider.class.getName()); - private final InstantiatingGrpcChannelProvider delegate; - private final ChannelPrimer channelPrimer; - @Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer; - @Nullable private final ScheduledExecutorService backgroundExecutor; - DirectPathCompatibleTracer directPathCompatibleTracer; - - private BigtableTransportChannelProvider( - InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, - ChannelPrimer channelPrimer, - @Nullable ChannelPoolMetricsTracer channelPoolMetricsTracer, - @Nullable ScheduledExecutorService backgroundExecutor, - DirectPathCompatibleTracer directPathCompatibleTracer) { - delegate = Preconditions.checkNotNull(instantiatingGrpcChannelProvider); - this.channelPrimer = channelPrimer; - this.channelPoolMetricsTracer = channelPoolMetricsTracer; - this.backgroundExecutor = backgroundExecutor; - this.directPathCompatibleTracer = directPathCompatibleTracer; + private static final Logger LOG = + Logger.getLogger(BigtableTransportChannelProvider.class.getName()); + private final InstantiatingGrpcChannelProvider delegate; + private final ChannelPrimer channelPrimer; + @Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer; + @Nullable private final ScheduledExecutorService backgroundExecutor; + DirectPathCompatibleTracer directPathCompatibleTracer; + + private BigtableTransportChannelProvider( + InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, + ChannelPrimer channelPrimer, + @Nullable ChannelPoolMetricsTracer channelPoolMetricsTracer, + @Nullable ScheduledExecutorService backgroundExecutor, + DirectPathCompatibleTracer directPathCompatibleTracer) { + delegate = Preconditions.checkNotNull(instantiatingGrpcChannelProvider); + this.channelPrimer = channelPrimer; + this.channelPoolMetricsTracer = channelPoolMetricsTracer; + this.backgroundExecutor = backgroundExecutor; + this.directPathCompatibleTracer = directPathCompatibleTracer; + } + + @Override + public boolean shouldAutoClose() { + return delegate.shouldAutoClose(); + } + + @SuppressWarnings("deprecation") + @Override + public boolean needsExecutor() { + return delegate.needsExecutor(); + } + + @Override + public BigtableTransportChannelProvider withExecutor(ScheduledExecutorService executor) { + return withExecutor((Executor) executor); + } + + // This executor if set is for handling rpc callbacks so we can't use it as the background + // executor + @Override + public BigtableTransportChannelProvider withExecutor(Executor executor) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withExecutor(executor); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); + } + + @Override + public boolean needsBackgroundExecutor() { + return delegate.needsBackgroundExecutor(); + } + + @Override + public TransportChannelProvider withBackgroundExecutor(ScheduledExecutorService executor) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withBackgroundExecutor(executor); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + executor, + directPathCompatibleTracer); + } + + @Override + public boolean needsHeaders() { + return delegate.needsHeaders(); + } + + @Override + public BigtableTransportChannelProvider withHeaders(Map headers) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withHeaders(headers); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); + } + + @Override + public boolean needsEndpoint() { + return delegate.needsEndpoint(); + } + + @Override + public TransportChannelProvider withEndpoint(String endpoint) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withEndpoint(endpoint); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); + } + + @Deprecated + @Override + public boolean acceptsPoolSize() { + return delegate.acceptsPoolSize(); + } + + @Deprecated + @Override + public TransportChannelProvider withPoolSize(int size) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withPoolSize(size); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); + } + + // We need this for direct access checker. + + /** Expected to only be called once when BigtableClientContext is created */ + @Override + public TransportChannel getTransportChannel() throws IOException { + InstantiatingGrpcChannelProvider directAccessProvider = + EnhancedBigtableStubSettings.applyDirectAccessTraits(delegate.toBuilder()) + .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) + .build(); + + Supplier maybeDirectAccessChannelFactory = + () -> { + try { + GrpcTransportChannel channel = + (GrpcTransportChannel) directAccessProvider.getTransportChannel(); + return (ManagedChannel) channel.getChannel(); + } catch (IOException e) { + throw new java.io.UncheckedIOException(e); + } + }; + + DirectAccessChecker directAccessChecker = ClassicDirectAccessChecker.create(channelPrimer); + boolean isDirectAccessEligible = false; + + try { + isDirectAccessEligible = + directAccessChecker.check(maybeDirectAccessChannelFactory, directPathCompatibleTracer); + } catch (Exception e) { + LOG.log(Level.FINE, "Client is not direct access eligible, using standard transport.", e); } - @Override - public boolean shouldAutoClose() { - return delegate.shouldAutoClose(); - } - - @SuppressWarnings("deprecation") - @Override - public boolean needsExecutor() { - return delegate.needsExecutor(); - } - - @Override - public BigtableTransportChannelProvider withExecutor(ScheduledExecutorService executor) { - return withExecutor((Executor) executor); - } - - // This executor if set is for handling rpc callbacks so we can't use it as the background - // executor - @Override - public BigtableTransportChannelProvider withExecutor(Executor executor) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withExecutor(executor); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - backgroundExecutor, - directPathCompatibleTracer); - } - - @Override - public boolean needsBackgroundExecutor() { - return delegate.needsBackgroundExecutor(); - } - - @Override - public TransportChannelProvider withBackgroundExecutor(ScheduledExecutorService executor) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withBackgroundExecutor(executor); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - executor, - directPathCompatibleTracer); - } - - @Override - public boolean needsHeaders() { - return delegate.needsHeaders(); - } - - @Override - public BigtableTransportChannelProvider withHeaders(Map headers) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withHeaders(headers); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - backgroundExecutor, - directPathCompatibleTracer); - } - - @Override - public boolean needsEndpoint() { - return delegate.needsEndpoint(); - } - - @Override - public TransportChannelProvider withEndpoint(String endpoint) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withEndpoint(endpoint); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - backgroundExecutor, - directPathCompatibleTracer); - } - - @Deprecated - @Override - public boolean acceptsPoolSize() { - return delegate.acceptsPoolSize(); - } + InstantiatingGrpcChannelProvider selectedProvider; - @Deprecated - @Override - public TransportChannelProvider withPoolSize(int size) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withPoolSize(size); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - backgroundExecutor, - directPathCompatibleTracer); + if (isDirectAccessEligible) { + selectedProvider = directAccessProvider; + } else { + selectedProvider = delegate; } - // We need this for direct access checker. - - /** Expected to only be called once when BigtableClientContext is created */ - @Override - public TransportChannel getTransportChannel() throws IOException { - InstantiatingGrpcChannelProvider directAccessProvider = - EnhancedBigtableStubSettings.applyDirectAccessTraits(delegate.toBuilder()) - .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) - .build(); - - BigtableChannelFactory maybeDirectAccessChannelFactory = - () -> { - GrpcTransportChannel channel = - (GrpcTransportChannel) directAccessProvider.getTransportChannel(); - return (ManagedChannel) channel.getChannel(); - }; - - DirectAccessChecker directAccessChecker = UnaryDirectAccessChecker.create(channelPrimer); - boolean isDirectAccessEligible = false; - - try { - isDirectAccessEligible = - directAccessChecker.check(maybeDirectAccessChannelFactory, directPathCompatibleTracer); - } catch (Exception e) { - LOG.log(Level.FINE, "Client is not direct access eligible, using standard transport.", e); - } - - InstantiatingGrpcChannelProvider selectedProvider; - - if (isDirectAccessEligible) { - selectedProvider = directAccessProvider; - } else { - selectedProvider = delegate; - } - - // This provider's main purpose is to replace the default GAX ChannelPool - // with a custom BigtableChannelPool, reusing the delegate's configuration. - - // To create our pool, we need a factory for raw gRPC channels. - // We achieve this by configuring our delegate to not use its own pooling - // (by setting pool size to 1) and then calling getTransportChannel() on it. - InstantiatingGrpcChannelProvider singleChannelProvider = - selectedProvider.toBuilder() - .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) - .build(); - - BigtableChannelFactory channelFactory = - () -> { - try { - GrpcTransportChannel channel = - (GrpcTransportChannel) singleChannelProvider.getTransportChannel(); - return (ManagedChannel) channel.getChannel(); - } catch (IOException e) { - throw new java.io.UncheckedIOException(e); - } - }; - - BigtableChannelPoolSettings btPoolSettings = - BigtableChannelPoolSettings.copyFrom(delegate.getChannelPoolSettings()); - - BigtableChannelPool btChannelPool = - BigtableChannelPool.create( - btPoolSettings, channelFactory, channelPrimer, backgroundExecutor); - - if (channelPoolMetricsTracer != null) { - channelPoolMetricsTracer.registerChannelInsightsProvider(btChannelPool); - channelPoolMetricsTracer.registerLoadBalancingStrategy( - btPoolSettings.getLoadBalancingStrategy()); - } - - return GrpcTransportChannel.create(btChannelPool); + // This provider's main purpose is to replace the default GAX ChannelPool + // with a custom BigtableChannelPool, reusing the delegate's configuration. + + // To create our pool, we need a factory for raw gRPC channels. + // We achieve this by configuring our delegate to not use its own pooling + // (by setting pool size to 1) and then calling getTransportChannel() on it. + InstantiatingGrpcChannelProvider singleChannelProvider = + selectedProvider.toBuilder() + .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) + .build(); + + BigtableChannelSupplier channelFactory = + () -> { + try { + GrpcTransportChannel channel = + (GrpcTransportChannel) singleChannelProvider.getTransportChannel(); + return (ManagedChannel) channel.getChannel(); + } catch (IOException e) { + throw new java.io.UncheckedIOException(e); + } + }; + + BigtableChannelPoolSettings btPoolSettings = + BigtableChannelPoolSettings.copyFrom(delegate.getChannelPoolSettings()); + + BigtableChannelPool btChannelPool = + BigtableChannelPool.create( + btPoolSettings, channelFactory, channelPrimer, backgroundExecutor); + + if (channelPoolMetricsTracer != null) { + channelPoolMetricsTracer.registerChannelInsightsProvider(btChannelPool); + channelPoolMetricsTracer.registerLoadBalancingStrategy( + btPoolSettings.getLoadBalancingStrategy()); } - @Override - public String getTransportName() { - return "bigtable"; - } - - @Override - public boolean needsCredentials() { - return delegate.needsCredentials(); - } - - @Override - public TransportChannelProvider withCredentials(Credentials credentials) { - InstantiatingGrpcChannelProvider newChannelProvider = - (InstantiatingGrpcChannelProvider) delegate.withCredentials(credentials); - return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - backgroundExecutor, - directPathCompatibleTracer); - } - - /** Creates a BigtableTransportChannelProvider. */ - public static BigtableTransportChannelProvider create( - InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, - ChannelPrimer channelPrimer, - ChannelPoolMetricsTracer outstandingRpcsMetricTracker, - ScheduledExecutorService backgroundExecutor, - DirectPathCompatibleTracer directPathCompatibleTracer) { - return new BigtableTransportChannelProvider( - instantiatingGrpcChannelProvider, - channelPrimer, - outstandingRpcsMetricTracker, - backgroundExecutor, - directPathCompatibleTracer); - } -} \ No newline at end of file + return GrpcTransportChannel.create(btChannelPool); + } + + @Override + public String getTransportName() { + return "bigtable"; + } + + @Override + public boolean needsCredentials() { + return delegate.needsCredentials(); + } + + @Override + public TransportChannelProvider withCredentials(Credentials credentials) { + InstantiatingGrpcChannelProvider newChannelProvider = + (InstantiatingGrpcChannelProvider) delegate.withCredentials(credentials); + return new BigtableTransportChannelProvider( + newChannelProvider, + channelPrimer, + channelPoolMetricsTracer, + backgroundExecutor, + directPathCompatibleTracer); + } + + /** Creates a BigtableTransportChannelProvider. */ + public static BigtableTransportChannelProvider create( + InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, + ChannelPrimer channelPrimer, + ChannelPoolMetricsTracer outstandingRpcsMetricTracker, + ScheduledExecutorService backgroundExecutor, + DirectPathCompatibleTracer directPathCompatibleTracer) { + return new BigtableTransportChannelProvider( + instantiatingGrpcChannelProvider, + channelPrimer, + outstandingRpcsMetricTracker, + backgroundExecutor, + directPathCompatibleTracer); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java index f5c9472b47..cbe6d45457 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java @@ -285,8 +285,9 @@ public void testCreateWithRefreshingChannel() throws Exception { Mockito.verify(credentialsProvider, Mockito.times(2)).getCredentials(); Mockito.verify(executorProvider, Mockito.times(1)).getExecutor(); Mockito.verify(watchdogProvider, Mockito.times(1)).getWatchdog(); - - assertThat(warmedChannels).hasSize(poolSize); + // +1 accounts for the temporary channel created by UnaryDirectAccessChecker + int expectedChannelCount = poolSize + 1; + assertThat(warmedChannels).hasSize(expectedChannelCount); assertThat(warmedChannels.values()).doesNotContain(false); // Wait for all the connections to close asynchronously @@ -294,7 +295,7 @@ public void testCreateWithRefreshingChannel() throws Exception { long sleepTimeMs = 1000; Thread.sleep(sleepTimeMs); // Verify that all the channels are closed - assertThat(terminateAttributes).hasSize(poolSize); + assertThat(terminateAttributes).hasSize(expectedChannelCount); } @Test diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java similarity index 58% rename from google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java rename to google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java index 275c6313ce..39fcfb6a00 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/UnaryDirectAccessCheckerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.cloud.bigtable.data.v2.stub; +package com.google.cloud.bigtable.data.v2.internal.dp; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -21,36 +21,45 @@ import com.google.bigtable.v2.PeerInfo; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; +import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor; import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer; import io.grpc.Channel; import io.grpc.ManagedChannel; +import java.util.function.Supplier; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; -import org.mockito.MockedConstruction; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @RunWith(JUnit4.class) -public class UnaryDirectAccessCheckerTest { +public class ClassicDirectAccessCheckerTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @Mock private ChannelPrimer mockChannelPrimer; - @Mock private BigtableChannelFactory mockChannelFactory; + @Mock private Supplier mockChannelFactory; @Mock private DirectPathCompatibleTracer mockTracer; @Mock private ManagedChannel mockChannel; + @Mock private MetadataExtractorInterceptor mockInterceptor; @Mock private MetadataExtractorInterceptor.SidebandData mockSidebandData; - private UnaryDirectAccessChecker checker; + private ClassicDirectAccessChecker checker; @Before public void setUp() throws Exception { - checker = UnaryDirectAccessChecker.create(mockChannelPrimer); - when(mockChannelFactory.createSingleChannel()).thenReturn(mockChannel); + // 1. Create a SPY of the checker so we can override just one method + checker = spy(ClassicDirectAccessChecker.create(mockChannelPrimer)); + + // 2. Tell the spy to return our mock interceptor instead of calling new() + doReturn(mockInterceptor).when(checker).createInterceptor(); + + // 3. Setup standard mocks + when(mockChannelFactory.get()).thenReturn(mockChannel); + when(mockInterceptor.getSidebandData()).thenReturn(mockSidebandData); } @Test @@ -60,66 +69,42 @@ public void testEligibleForDirectAccess() { .setTransportType(PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) .build(); when(mockSidebandData.getPeerInfo()).thenReturn(peerInfo); - when(mockSidebandData.getIpProtocol()) .thenReturn(MetadataExtractorInterceptor.SidebandData.IpProtocol.IPV6); - try (MockedConstruction ignored = - mockConstruction( - MetadataExtractorInterceptor.class, - (mock, context) -> { - when(mock.getSidebandData()).thenReturn(mockSidebandData); - })) { - - boolean isEligible = checker.check(mockChannelFactory, mockTracer); + boolean isEligible = checker.check(mockChannelFactory, mockTracer); - assertThat(isEligible).isFalse(); - verify(mockChannelPrimer).primeChannel(any(Channel.class)); - verify(mockTracer).recordSuccess("ipv6"); - verify(mockChannel).shutdownNow(); - } + assertThat(isEligible).isTrue(); + verify(mockChannelPrimer).primeChannel(any(Channel.class)); + verify(mockTracer).recordSuccess("ipv6"); + verify(mockChannel).shutdownNow(); } @Test public void testNotEligibleProxiedRouting() { - // 1. Setup sideband data to simulate standard CloudPath routing PeerInfo peerInfo = PeerInfo.newBuilder() .setTransportType(PeerInfo.TransportType.TRANSPORT_TYPE_CLOUD_PATH) .build(); when(mockSidebandData.getPeerInfo()).thenReturn(peerInfo); - try (MockedConstruction mocked = - mockConstruction( - MetadataExtractorInterceptor.class, - (mock, context) -> { - when(mock.getSidebandData()).thenReturn(mockSidebandData); - })) { - - boolean isEligible = checker.check(mockChannelFactory, mockTracer); + boolean isEligible = checker.check(mockChannelFactory, mockTracer); - assertThat(isEligible).isFalse(); - verifyNoInteractions(mockTracer); - verify(mockChannel).shutdownNow(); - } + assertThat(isEligible).isFalse(); + verifyNoInteractions(mockTracer); + verify(mockChannel).shutdownNow(); } @Test public void testMissingSidebandData() { - // Interceptor failed to capture anything (returns null) - try (MockedConstruction mocked = - mockConstruction( - MetadataExtractorInterceptor.class, - (mock, context) -> { - when(mock.getSidebandData()).thenReturn(null); - })) { - - boolean isEligible = checker.check(mockChannelFactory, mockTracer); - - assertThat(isEligible).isFalse(); - verifyNoInteractions(mockTracer); - verify(mockChannel).shutdownNow(); - } + // Override the Before setup to return null for this specific test + when(mockInterceptor.getSidebandData()).thenReturn(null); + + boolean isEligible = checker.check(mockChannelFactory, mockTracer); + + assertThat(isEligible).isFalse(); + verifyNoInteractions(mockTracer); + verify(mockChannel).shutdownNow(); } @Test diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java index a9eda04356..1037aa1c15 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java @@ -552,7 +552,8 @@ public void testChannelPrimerConfigured() throws IOException { defaultSettings.toBuilder().setRefreshingChannel(true).build(); try (EnhancedBigtableStub ignored = EnhancedBigtableStub.create(settings)) { - assertThat(fakeDataService.pingRequests).hasSize(1); + // direct access checker ping + assertThat(fakeDataService.pingRequests).hasSize(2); } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolTest.java index 1cd98a92c4..6719a70d89 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolTest.java @@ -19,7 +19,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import com.google.cloud.bigtable.data.v2.stub.BigtableChannelFactory; import com.google.common.collect.Iterables; import io.grpc.CallOptions; import io.grpc.ClientCall; @@ -32,6 +31,7 @@ import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Supplier; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -48,7 +48,7 @@ public class BigtableChannelPoolTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); - @Mock private BigtableChannelFactory mockChannelFactory; + @Mock private Supplier mockChannelFactory; @Mock private ChannelPrimer mockChannelPrimer; @Mock private ManagedChannel mockChannel; @Mock private ClientCall mockClientCall; @@ -75,7 +75,7 @@ public String parse(InputStream stream) { @Before public void setUp() throws IOException { - when(mockChannelFactory.createSingleChannel()).thenReturn(mockChannel); + when(mockChannelFactory.get()).thenReturn(mockChannel); when(mockChannel.newCall( ArgumentMatchers.>any(), any(CallOptions.class))) .thenReturn(mockClientCall); From 1ca692cfc49525b261d396c79b8c4e0c7da762ab Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Sat, 21 Mar 2026 00:27:24 -0400 Subject: [PATCH 07/28] address review comments --- .../bigtable/gaxx/grpc/BigtableTransportChannelProvider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java index 8f3342bba4..0b7c44cff4 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java @@ -78,6 +78,7 @@ public boolean needsExecutor() { } @Override + @Deprecated public BigtableTransportChannelProvider withExecutor(ScheduledExecutorService executor) { return withExecutor((Executor) executor); } From cc306bc27fba5dfa40a986d97a311bb5bc02d8dc Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Sat, 21 Mar 2026 00:48:47 -0400 Subject: [PATCH 08/28] address review comments --- .../bigtable/data/v2/stub/EnhancedBigtableStubSettings.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index 76c3e49b51..b7189ecc04 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -637,6 +637,8 @@ private Builder() { // only if Traffic Director sends the request (with grpc as target type) // For GFE/CFE, sending setDirectAccessRequested // is fine as GFE/CFE sends with gslb target type + // TODO: flip the bit setDirectAccessRequested and setTrafficDirectorEnabled once we make + // client compatible by default. featureFlags = FeatureFlags.newBuilder() .setReverseScans(true) From d8cf0e83e765f58b4233e7725a716bfe9dd56333 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Mon, 23 Mar 2026 16:50:34 -0400 Subject: [PATCH 09/28] fix --- .../dp/ClassicDirectAccessChecker.java | 15 ++--- .../v2/internal/dp/DirectAccessChecker.java | 4 +- .../data/v2/stub/BigtableClientContext.java | 4 +- .../v2/stub/EnhancedBigtableStubSettings.java | 30 ++++++++- .../BigtableTransportChannelProvider.java | 65 +++++++++++-------- .../v2/BigtableDataClientFactoryTest.java | 3 +- .../dp/ClassicDirectAccessCheckerTest.java | 14 ++-- .../EnhancedBigtableStubSettingsTest.java | 1 + .../v2/stub/EnhancedBigtableStubTest.java | 14 ++++ 9 files changed, 97 insertions(+), 53 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java index cfd14741cc..db26efbdb7 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java @@ -28,7 +28,6 @@ import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.Nullable; /** * Evaluates whether a given channel has Direct Access (DirectPath) routing by executing a RPC and @@ -37,27 +36,25 @@ @InternalApi public class ClassicDirectAccessChecker implements DirectAccessChecker { private static final Logger LOG = Logger.getLogger(ClassicDirectAccessChecker.class.getName()); + private final DirectPathCompatibleTracer tracer; private final ChannelPrimer channelPrimer; - private ClassicDirectAccessChecker(ChannelPrimer channelPrimer) { + public ClassicDirectAccessChecker( + DirectPathCompatibleTracer tracer, ChannelPrimer channelPrimer) { + this.tracer = tracer; this.channelPrimer = channelPrimer; } - public static ClassicDirectAccessChecker create(ChannelPrimer channelPrimer) { - return new ClassicDirectAccessChecker(channelPrimer); - } - @VisibleForTesting MetadataExtractorInterceptor createInterceptor() { return new MetadataExtractorInterceptor(); } @Override - public boolean check( - Supplier channelSupplier, @Nullable DirectPathCompatibleTracer tracer) { + public boolean check(Supplier supplier) { ManagedChannel channel = null; try { - channel = channelSupplier.get(); + channel = supplier.get(); MetadataExtractorInterceptor interceptor = createInterceptor(); Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor); channelPrimer.primeChannel(interceptedChannel); diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java index beab21e8d4..519fadf4a0 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java @@ -16,10 +16,8 @@ package com.google.cloud.bigtable.data.v2.internal.dp; import com.google.api.core.InternalApi; -import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; import io.grpc.ManagedChannel; import java.util.function.Supplier; -import javax.annotation.Nullable; @InternalApi /* Evaluates whether a given channel supports Direct Access. */ @@ -30,5 +28,5 @@ public interface DirectAccessChecker { * @param supplier A supplier to create maybe direct access channel * @return true if the channel is eligible for Direct Access */ - boolean check(Supplier supplier, @Nullable DirectPathCompatibleTracer tracer); + boolean check(Supplier supplier); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java index 2680558995..7eb39ea8ef 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java @@ -160,14 +160,14 @@ public static BigtableClientContext create( credentials, builder.getHeaderProvider().getHeaders()); } - BigtableTransportChannelProvider btTransportProvider = BigtableTransportChannelProvider.create( transportProvider.build(), channelPrimer, metrics.getChannelPoolMetricsTracer(), backgroundExecutor, - metrics.getDirectPathCompatibleTracer()); + metrics.getDirectPathCompatibleTracer(), + builder.isDirectPathEnabledByDefault()); builder.setTransportChannelProvider(btTransportProvider); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index b7189ecc04..dda8a42701 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -101,6 +101,8 @@ public class EnhancedBigtableStubSettings extends StubSettings { - + private boolean enableDirectPathByDefault; private String projectId; private String instanceId; private String appProfileId; @@ -639,12 +648,16 @@ private Builder() { // is fine as GFE/CFE sends with gslb target type // TODO: flip the bit setDirectAccessRequested and setTrafficDirectorEnabled once we make // client compatible by default. + + this.enableDirectPathByDefault = + Boolean.parseBoolean(System.getenv("CBT_ENABLE_DIRECTPATH_BY_DEFAULT")); + featureFlags = FeatureFlags.newBuilder() .setReverseScans(true) .setLastScannedRowResponses(true) - .setDirectAccessRequested(DIRECT_PATH_ENABLED) - .setTrafficDirectorEnabled(DIRECT_PATH_ENABLED) + .setDirectAccessRequested(DIRECT_PATH_ENABLED || enableDirectPathByDefault) + .setTrafficDirectorEnabled(DIRECT_PATH_ENABLED || enableDirectPathByDefault) .setPeerInfo(true); } @@ -658,6 +671,7 @@ private Builder(EnhancedBigtableStubSettings settings) { metricsEndpoint = settings.getMetricsEndpoint(); areInternalMetricsEnabled = settings.areInternalMetricsEnabled; jwtAudience = settings.jwtAudience; + this.enableDirectPathByDefault = settings.isDirectPathEnabledByDefault(); this.perOpSettings = new ClientOperationSettings.Builder(settings.perOpSettings); @@ -743,6 +757,15 @@ public Builder setRefreshingChannel(boolean isRefreshingChannel) { return this; } + public Builder setEnableDirectPathByDefault(boolean enableDirectPathByDefault) { + this.enableDirectPathByDefault = enableDirectPathByDefault; + return this; + } + + public boolean isDirectPathEnabledByDefault() { + return enableDirectPathByDefault; + } + /** * @deprecated This field is ignored. If {@link #isRefreshingChannel()} is enabled, warm up * requests will be sent to all table ids of the instance. @@ -1024,6 +1047,7 @@ public String toString() { .add("metricsEndpoint", metricsEndpoint) .add("areInternalMetricsEnabled", areInternalMetricsEnabled) .add("jwtAudience", jwtAudience) + .add("enableDirectPathByDefault", enableDirectPathByDefault) .add("parent", super.toString()) .toString(); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java index 0b7c44cff4..a24c17cf65 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java @@ -52,18 +52,21 @@ public final class BigtableTransportChannelProvider implements TransportChannelP @Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer; @Nullable private final ScheduledExecutorService backgroundExecutor; DirectPathCompatibleTracer directPathCompatibleTracer; + private final boolean enableDirectAccessChecker; private BigtableTransportChannelProvider( InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, ChannelPrimer channelPrimer, @Nullable ChannelPoolMetricsTracer channelPoolMetricsTracer, @Nullable ScheduledExecutorService backgroundExecutor, - DirectPathCompatibleTracer directPathCompatibleTracer) { + DirectPathCompatibleTracer directPathCompatibleTracer, + boolean enableDirectAccessChecker) { delegate = Preconditions.checkNotNull(instantiatingGrpcChannelProvider); this.channelPrimer = channelPrimer; this.channelPoolMetricsTracer = channelPoolMetricsTracer; this.backgroundExecutor = backgroundExecutor; this.directPathCompatibleTracer = directPathCompatibleTracer; + this.enableDirectAccessChecker = enableDirectAccessChecker; } @Override @@ -94,7 +97,8 @@ public BigtableTransportChannelProvider withExecutor(Executor executor) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer); + directPathCompatibleTracer, + enableDirectAccessChecker); } @Override @@ -111,7 +115,8 @@ public TransportChannelProvider withBackgroundExecutor(ScheduledExecutorService channelPrimer, channelPoolMetricsTracer, executor, - directPathCompatibleTracer); + directPathCompatibleTracer, + enableDirectAccessChecker); } @Override @@ -128,7 +133,8 @@ public BigtableTransportChannelProvider withHeaders(Map headers) channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer); + directPathCompatibleTracer, + enableDirectAccessChecker); } @Override @@ -145,7 +151,8 @@ public TransportChannelProvider withEndpoint(String endpoint) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer); + directPathCompatibleTracer, + enableDirectAccessChecker); } @Deprecated @@ -164,7 +171,8 @@ public TransportChannelProvider withPoolSize(int size) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer); + directPathCompatibleTracer, + enableDirectAccessChecker); } // We need this for direct access checker. @@ -177,25 +185,27 @@ public TransportChannel getTransportChannel() throws IOException { .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) .build(); - Supplier maybeDirectAccessChannelFactory = - () -> { - try { - GrpcTransportChannel channel = - (GrpcTransportChannel) directAccessProvider.getTransportChannel(); - return (ManagedChannel) channel.getChannel(); - } catch (IOException e) { - throw new java.io.UncheckedIOException(e); - } - }; - - DirectAccessChecker directAccessChecker = ClassicDirectAccessChecker.create(channelPrimer); + DirectAccessChecker directAccessChecker = + new ClassicDirectAccessChecker(directPathCompatibleTracer, channelPrimer); boolean isDirectAccessEligible = false; - try { - isDirectAccessEligible = - directAccessChecker.check(maybeDirectAccessChannelFactory, directPathCompatibleTracer); - } catch (Exception e) { - LOG.log(Level.FINE, "Client is not direct access eligible, using standard transport.", e); + if (enableDirectAccessChecker) { + Supplier maybeDirectAccessChannelSupplier = + () -> { + try { + GrpcTransportChannel channel = + (GrpcTransportChannel) directAccessProvider.getTransportChannel(); + return (ManagedChannel) channel.getChannel(); + } catch (IOException e) { + throw new java.io.UncheckedIOException(e); + } + }; + + try { + isDirectAccessEligible = directAccessChecker.check(maybeDirectAccessChannelSupplier); + } catch (Exception e) { + LOG.log(Level.FINE, "Client is not direct access eligible, using standard transport.", e); + } } InstantiatingGrpcChannelProvider selectedProvider; @@ -263,7 +273,8 @@ public TransportChannelProvider withCredentials(Credentials credentials) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer); + directPathCompatibleTracer, + enableDirectAccessChecker); } /** Creates a BigtableTransportChannelProvider. */ @@ -272,12 +283,14 @@ public static BigtableTransportChannelProvider create( ChannelPrimer channelPrimer, ChannelPoolMetricsTracer outstandingRpcsMetricTracker, ScheduledExecutorService backgroundExecutor, - DirectPathCompatibleTracer directPathCompatibleTracer) { + DirectPathCompatibleTracer directPathCompatibleTracer, + boolean enableDirectAccessChecker) { return new BigtableTransportChannelProvider( instantiatingGrpcChannelProvider, channelPrimer, outstandingRpcsMetricTracker, backgroundExecutor, - directPathCompatibleTracer); + directPathCompatibleTracer, + enableDirectAccessChecker); } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java index cbe6d45457..62ce89d4b9 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java @@ -267,7 +267,8 @@ public void testCreateWithRefreshingChannel() throws Exception { .stubSettings() .setCredentialsProvider(credentialsProvider) .setStreamWatchdogProvider(watchdogProvider) - .setBackgroundExecutorProvider(executorProvider); + .setBackgroundExecutorProvider(executorProvider) + .setEnableDirectPathByDefault(true); InstantiatingGrpcChannelProvider channelProvider = (InstantiatingGrpcChannelProvider) builder.stubSettings().getTransportChannelProvider(); InstantiatingGrpcChannelProvider.Builder channelProviderBuilder = channelProvider.toBuilder(); diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java index 39fcfb6a00..f6c6e27915 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java @@ -51,13 +51,9 @@ public class ClassicDirectAccessCheckerTest { @Before public void setUp() throws Exception { - // 1. Create a SPY of the checker so we can override just one method - checker = spy(ClassicDirectAccessChecker.create(mockChannelPrimer)); - - // 2. Tell the spy to return our mock interceptor instead of calling new() + checker = spy(new ClassicDirectAccessChecker(mockTracer, mockChannelPrimer)); doReturn(mockInterceptor).when(checker).createInterceptor(); - // 3. Setup standard mocks when(mockChannelFactory.get()).thenReturn(mockChannel); when(mockInterceptor.getSidebandData()).thenReturn(mockSidebandData); } @@ -72,7 +68,7 @@ public void testEligibleForDirectAccess() { when(mockSidebandData.getIpProtocol()) .thenReturn(MetadataExtractorInterceptor.SidebandData.IpProtocol.IPV6); - boolean isEligible = checker.check(mockChannelFactory, mockTracer); + boolean isEligible = checker.check(mockChannelFactory); assertThat(isEligible).isTrue(); verify(mockChannelPrimer).primeChannel(any(Channel.class)); @@ -88,7 +84,7 @@ public void testNotEligibleProxiedRouting() { .build(); when(mockSidebandData.getPeerInfo()).thenReturn(peerInfo); - boolean isEligible = checker.check(mockChannelFactory, mockTracer); + boolean isEligible = checker.check(mockChannelFactory); assertThat(isEligible).isFalse(); verifyNoInteractions(mockTracer); @@ -100,7 +96,7 @@ public void testMissingSidebandData() { // Override the Before setup to return null for this specific test when(mockInterceptor.getSidebandData()).thenReturn(null); - boolean isEligible = checker.check(mockChannelFactory, mockTracer); + boolean isEligible = checker.check(mockChannelFactory); assertThat(isEligible).isFalse(); verifyNoInteractions(mockTracer); @@ -113,7 +109,7 @@ public void testExceptionSafetyAndCleanup() { .when(mockChannelPrimer) .primeChannel(any(Channel.class)); - boolean isEligible = checker.check(mockChannelFactory, mockTracer); + boolean isEligible = checker.check(mockChannelFactory); assertThat(isEligible).isFalse(); verifyNoInteractions(mockTracer); diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java index bf16ab1c62..520204ed85 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java @@ -905,6 +905,7 @@ public void isRefreshingChannelFalseValueTest() { "metricsEndpoint", "areInternalMetricsEnabled", "jwtAudience", + "enableDirectPathByDefault", }; @Test diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java index 1037aa1c15..276784bf14 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java @@ -551,6 +551,20 @@ public void testChannelPrimerConfigured() throws IOException { EnhancedBigtableStubSettings settings = defaultSettings.toBuilder().setRefreshingChannel(true).build(); + try (EnhancedBigtableStub ignored = EnhancedBigtableStub.create(settings)) { + // direct access checker ping + assertThat(fakeDataService.pingRequests).hasSize(1); + } + } + + @Test + public void testChannelPrimeWithDirectAccessEnabledByDefault() throws IOException { + EnhancedBigtableStubSettings settings = + defaultSettings.toBuilder() + .setRefreshingChannel(true) + .setEnableDirectPathByDefault(true) + .build(); + try (EnhancedBigtableStub ignored = EnhancedBigtableStub.create(settings)) { // direct access checker ping assertThat(fakeDataService.pingRequests).hasSize(2); From 819444e62153ba8ad30d6a9c3ffe1bfb7c51f7c3 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Mon, 23 Mar 2026 16:54:45 -0400 Subject: [PATCH 10/28] comments --- .../bigtable/data/v2/stub/EnhancedBigtableStubSettings.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index dda8a42701..3b036e9221 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -648,10 +648,6 @@ private Builder() { // is fine as GFE/CFE sends with gslb target type // TODO: flip the bit setDirectAccessRequested and setTrafficDirectorEnabled once we make // client compatible by default. - - this.enableDirectPathByDefault = - Boolean.parseBoolean(System.getenv("CBT_ENABLE_DIRECTPATH_BY_DEFAULT")); - featureFlags = FeatureFlags.newBuilder() .setReverseScans(true) From ceaea99d9b70e74a3ef10c07ce1ea9ac5b01f867 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Mon, 23 Mar 2026 16:58:45 -0400 Subject: [PATCH 11/28] fix --- .../bigtable/gaxx/grpc/BigtableTransportChannelProvider.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java index a24c17cf65..7e9fa4333e 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java @@ -26,7 +26,6 @@ import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; import com.google.cloud.bigtable.data.v2.internal.dp.ClassicDirectAccessChecker; import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessChecker; -import com.google.cloud.bigtable.data.v2.stub.BigtableChannelSupplier; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStubSettings; import com.google.common.base.Preconditions; import io.grpc.ManagedChannel; @@ -227,7 +226,7 @@ public TransportChannel getTransportChannel() throws IOException { .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) .build(); - BigtableChannelSupplier channelFactory = + Supplier channelSupplier = () -> { try { GrpcTransportChannel channel = @@ -243,7 +242,7 @@ public TransportChannel getTransportChannel() throws IOException { BigtableChannelPool btChannelPool = BigtableChannelPool.create( - btPoolSettings, channelFactory, channelPrimer, backgroundExecutor); + btPoolSettings, channelSupplier, channelPrimer, backgroundExecutor); if (channelPoolMetricsTracer != null) { channelPoolMetricsTracer.registerChannelInsightsProvider(btChannelPool); From 23fbcd2dc5a282013c945fff632c8dd2783bb571 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Tue, 24 Mar 2026 01:31:22 -0400 Subject: [PATCH 12/28] no nullable --- .../cloud/bigtable/data/v2/internal/csm/MetricsImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java index 69391b9796..26cb73cce8 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java @@ -70,7 +70,7 @@ public class MetricsImpl implements Metrics, Closeable { @Nullable private final GrpcOpenTelemetry grpcOtel; @Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer; - @Nullable private final DirectPathCompatibleTracer directPathCompatibleTracer; + private final DirectPathCompatibleTracer directPathCompatibleTracer; @Nullable private final Pacemaker pacemaker; private final List> tasks = new ArrayList<>(); @@ -178,7 +178,6 @@ public ChannelPoolMetricsTracer getChannelPoolMetricsTracer() { return channelPoolMetricsTracer; } - @Nullable @Override public DirectPathCompatibleTracer getDirectPathCompatibleTracer() { return directPathCompatibleTracer; From 04ea5843ec2b1c528e74b499a9fd605a812c8a01 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Tue, 24 Mar 2026 01:37:13 -0400 Subject: [PATCH 13/28] no nullable --- .../bigtable/data/v2/stub/EnhancedBigtableStubSettings.java | 2 -- .../bigtable/gaxx/grpc/BigtableTransportChannelProvider.java | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index 3b036e9221..80f6495b8a 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -101,8 +101,6 @@ public class EnhancedBigtableStubSettings extends StubSettings Date: Thu, 26 Mar 2026 00:47:19 -0400 Subject: [PATCH 14/28] fix --- .../data/v2/stub/EnhancedBigtableStubSettings.java | 13 +++++++++++-- .../gaxx/grpc/BigtableTransportChannelProvider.java | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index 80f6495b8a..7e740a8f56 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -623,6 +623,15 @@ public static class Builder extends StubSettings.Builder Date: Thu, 26 Mar 2026 00:53:45 -0400 Subject: [PATCH 15/28] FIX --- .../v2/stub/EnhancedBigtableStubSettings.java | 31 ++----------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index 7e740a8f56..c54e05db3e 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -96,11 +96,6 @@ public class EnhancedBigtableStubSettings extends StubSettings Date: Sun, 29 Mar 2026 15:38:48 -0400 Subject: [PATCH 16/28] fix(bigtable): rely on metadata from afes for connectivity error --- .../internal/dp/DirectAccessInvestigator.java | 4 ++ .../data/v2/internal/dp/GCECheck.java | 38 +++++++++++++++++++ .../v2/internal/dp/LoopBackInterface.java | 4 ++ .../data/v2/internal/dp/MetadataServer.java | 4 ++ 4 files changed, 50 insertions(+) create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java new file mode 100644 index 0000000000..c5ab047ab7 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java @@ -0,0 +1,4 @@ +package com.google.cloud.bigtable.data.v2.internal.dp; + +public class DirectAccessInvestigator { +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java new file mode 100644 index 0000000000..49c315a7de --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java @@ -0,0 +1,38 @@ +package com.google.cloud.bigtable.data.v2.internal.dp; + +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +class GCECheck { + private static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google"; + private static final String GCE_PRODUCTION_NAME_AFTER_2016 = "Google Compute Engine"; + + @VisibleForTesting + static String systemProductName = null; + + static boolean isRunningOnGCP() { + String osName = System.getProperty("os.name"); + if ("Linux".equals(osName)) { + String productName = getSystemProductName(); + return productName.contains(GCE_PRODUCTION_NAME_PRIOR_2016) + || productName.contains(GCE_PRODUCTION_NAME_AFTER_2016); + } + return false; + } + + private static String getSystemProductName() { + if (systemProductName != null) { + return systemProductName; + } + try { + return new String( + Files.readAllBytes(Paths.get("/sys/class/dmi/id/product_name")), + StandardCharsets.UTF_8).trim(); + } catch (IOException e) { + return ""; + } + } +} \ No newline at end of file diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java new file mode 100644 index 0000000000..04b4ef7eb5 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java @@ -0,0 +1,4 @@ +package com.google.cloud.bigtable.data.v2.internal.dp; + +public class LoopbackInterface { +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java new file mode 100644 index 0000000000..f2cd76867d --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java @@ -0,0 +1,4 @@ +package com.google.cloud.bigtable.data.v2.internal.dp; + +public class MetadataServer { +} From f56667ac69a58fa85f19f3cc094f240fb00dbd5d Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Sun, 29 Mar 2026 16:35:42 -0400 Subject: [PATCH 17/28] fix and add direct access investigator --- .../data/v2/internal/csm/attributes/Util.java | 16 ++ .../DefaultDirectPathCompatibleTracer.java | 5 +- .../tracers/DirectPathCompatibleTracer.java | 5 +- .../NoopDirectPathCompatibleTracer.java | 3 +- .../dp/ClassicDirectAccessChecker.java | 60 ++++---- .../v2/internal/dp/DirectAccessChecker.java | 9 +- .../internal/dp/DirectAccessInvestigator.java | 137 ++++++++++++++++++ .../data/v2/internal/dp/GCECheck.java | 65 ++++++--- .../v2/internal/dp/LoopBackInterface.java | 86 ++++++++++- .../data/v2/internal/dp/MetadataServer.java | 96 +++++++++++- .../data/v2/stub/BigtableClientContext.java | 11 +- .../v2/stub/EnhancedBigtableStubSettings.java | 56 ++++--- .../v2/stub/MetadataExtractorInterceptor.java | 21 +-- .../BigtableTransportChannelProvider.java | 72 +++++---- .../dp/ClassicDirectAccessCheckerTest.java | 33 +++-- 15 files changed, 513 insertions(+), 162 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java index 221452537d..91cfab1301 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java @@ -40,6 +40,22 @@ import javax.annotation.Nullable; public class Util { + public enum IpProtocol { + IPV4("ipv4"), + IPV6("ipv6"), + UNKNOWN("unknown"); + + private final String value; + + IpProtocol(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + static final String TRANSPORT_TYPE_PREFIX = "TRANSPORT_TYPE_"; public static String formatTransportZone(@Nullable PeerInfo peerInfo) { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java index 465e147868..335c414cfa 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java @@ -18,6 +18,7 @@ import com.google.api.core.InternalApi; import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo; +import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util; @InternalApi public class DefaultDirectPathCompatibleTracer implements DirectPathCompatibleTracer { @@ -31,8 +32,8 @@ public DefaultDirectPathCompatibleTracer( } @Override - public void recordSuccess(String ipPreference) { - recorder.dpCompatGuage.recordSuccess(clientInfo, ipPreference); + public void recordSuccess(Util.IpProtocol ipProtocol) { + recorder.dpCompatGuage.recordSuccess(clientInfo, ipProtocol.getValue()); } @Override diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java index 4776785dc2..7ae71e9694 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java @@ -16,6 +16,7 @@ package com.google.cloud.bigtable.data.v2.internal.csm.tracers; import com.google.api.core.InternalApi; +import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util; /** Interface for recording DirectPath/DirectAccess eligibility metrics. */ @InternalApi @@ -24,9 +25,9 @@ public interface DirectPathCompatibleTracer { /** * Records that the environment is eligible and successfully connected via DirectPath. * - * @param ipPreference The IP preference used (e.g., "ipv6"). + * @param ipProtocol The IP protocol used (e.g., "ipv6"). */ - void recordSuccess(String ipPreference); + void recordSuccess(Util.IpProtocol ipProtocol); /** * Records that the environment is not eligible or failed to connect via DirectPath. diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java index bcfb8a48f7..88cc767fdd 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java @@ -16,6 +16,7 @@ package com.google.cloud.bigtable.data.v2.internal.csm.tracers; import com.google.api.core.InternalApi; +import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util; @InternalApi public class NoopDirectPathCompatibleTracer implements DirectPathCompatibleTracer { @@ -26,7 +27,7 @@ public class NoopDirectPathCompatibleTracer implements DirectPathCompatibleTrace private NoopDirectPathCompatibleTracer() {} @Override - public void recordSuccess(String ipPreference) { + public void recordSuccess(Util.IpProtocol ipProtocol) { // No-op } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java index db26efbdb7..dfb3bf029a 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java @@ -25,7 +25,6 @@ import io.grpc.ClientInterceptors; import io.grpc.ManagedChannel; import java.util.Optional; -import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -51,41 +50,40 @@ MetadataExtractorInterceptor createInterceptor() { } @Override - public boolean check(Supplier supplier) { - ManagedChannel channel = null; + public boolean check(Channel channel) { try { - channel = supplier.get(); - MetadataExtractorInterceptor interceptor = createInterceptor(); - Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor); - channelPrimer.primeChannel(interceptedChannel); - - // Extract the sideband data populated by the interceptor - MetadataExtractorInterceptor.SidebandData sidebandData = interceptor.getSidebandData(); - - boolean isEligible = - Optional.ofNullable(sidebandData) - .map(MetadataExtractorInterceptor.SidebandData::getPeerInfo) - .map(PeerInfo::getTransportType) - .map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) - .orElse(false); - - if (isEligible) { - String ipProtocolStr = - sidebandData.getIpProtocol() != null - ? sidebandData.getIpProtocol().toString().toLowerCase() - : "unknown"; - tracer.recordSuccess(ipProtocolStr); - } - - return isEligible; - + return evaluateEligibility(channel); } catch (Exception e) { - LOG.log(Level.FINE, "Failed to evaluate direct access eligibility.", e); + LOG.log(Level.WARNING, "Failed to evaluate direct access eligibility.", e); return false; } finally { - if (channel != null) { - channel.shutdownNow(); + if (channel instanceof ManagedChannel) { + ManagedChannel managedChannel = (ManagedChannel) channel; + managedChannel.shutdownNow(); } } } + + /** + * Executes the underlying RPC and evaluates the eligibility. + */ + private boolean evaluateEligibility(Channel channel) { + MetadataExtractorInterceptor interceptor = createInterceptor(); + Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor); + channelPrimer.primeChannel(interceptedChannel); + MetadataExtractorInterceptor.SidebandData sidebandData = interceptor.getSidebandData(); + + boolean isEligible = + Optional.ofNullable(sidebandData) + .map(MetadataExtractorInterceptor.SidebandData::getPeerInfo) + .map(PeerInfo::getTransportType) + .map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) + .orElse(false); + + if (isEligible) { + // getIp should be non-null as isEligible is true + tracer.recordSuccess(sidebandData.getIpProtocol()); + } + return isEligible; + } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java index 519fadf4a0..2f75f6503f 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java @@ -16,17 +16,16 @@ package com.google.cloud.bigtable.data.v2.internal.dp; import com.google.api.core.InternalApi; -import io.grpc.ManagedChannel; -import java.util.function.Supplier; +import io.grpc.Channel; @InternalApi /* Evaluates whether a given channel supports Direct Access. */ public interface DirectAccessChecker { /** - * Evaluates if Direct Access is available by creating a test channel. + * Evaluates if Direct Access is available by sending request via provided channel. * - * @param supplier A supplier to create maybe direct access channel + * @param channel A channel to probe direct access connectivity * @return true if the channel is eligible for Direct Access */ - boolean check(Supplier supplier); + boolean check(Channel channel); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java index c5ab047ab7..b32ace922c 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java @@ -1,4 +1,141 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.cloud.bigtable.data.v2.internal.dp; +import com.google.api.core.InternalApi; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; +import java.net.InetAddress; +import java.util.logging.Level; +import java.util.logging.Logger; + +@InternalApi public class DirectAccessInvestigator { + private static final Logger LOG = Logger.getLogger(DirectAccessInvestigator.class.getName()); + + // Telemetry metric reason codes + private static final String REASON_NOT_IN_GCP = "not_in_gcp"; + private static final String REASON_METADATA_UNREACHABLE = "metadata_unreachable"; + private static final String REASON_NO_IP_ASSIGNED = "no_ip_assigned"; + private static final String REASON_LOOPBACK_DOWN = "loopback_misconfigured"; + private static final String REASON_LOOPBACK_V4_MISSING = "loopback_misconfigured_ipv4"; + private static final String REASON_LOOPBACK_V6_MISSING = "loopback_misconfigured_ipv6"; + private static final String REASON_UNKNOWN = ""; + + public static void investigateAndReport( + DirectPathCompatibleTracer tracer, Throwable originalError) { + try { + if (!GCECheck.isRunningOnGCP()) { + recordAndLog( + tracer, REASON_NOT_IN_GCP, "Direct Access investigation: not in GCP.", originalError); + return; + } + + if (!MetadataServer.isReachable()) { + recordAndLog( + tracer, + REASON_METADATA_UNREACHABLE, + "Direct Access investigation: Metadata unreachable.", + null); + return; + } + + InetAddress ipv4 = MetadataServer.getIPv4(); + InetAddress ipv6 = MetadataServer.getIPv6(); + + if (ipv4 == null && ipv6 == null) { + recordAndLog( + tracer, + REASON_NO_IP_ASSIGNED, + "Direct Access investigation: Neither IPv4 nor IPv6 assigned.", + null); + return; + } + + if (!LoopBackInterface.isUp()) { + recordAndLog( + tracer, + REASON_LOOPBACK_DOWN, + "Direct Access investigation: Loopback interface down.", + null); + return; + } + + // ONLY check for IPv4 loopback if we are using an IPv4 address + if (ipv4 != null && !LoopBackInterface.hasLocalIpv4Loopback()) { + recordAndLog( + tracer, + REASON_LOOPBACK_V4_MISSING, + "Direct Access investigation: IPv4 loopback missing.", + null); + return; + } + + // ONLY check for IPv6 loopback if we are using an IPv6 address + if (ipv6 != null && !LoopBackInterface.hasLocalIpv6Loopback()) { + recordAndLog( + tracer, + REASON_LOOPBACK_V6_MISSING, + "Direct Access investigation: IPv6 loopback missing.", + null); + return; + } + + boolean v4Plumbed = ipv4 != null && LoopBackInterface.isIpPlumbed(ipv4); + if (ipv4 != null && !v4Plumbed) { + LOG.log( + Level.FINE, + "Direct Access investigation: IPv4 assigned by metadata but not found on NIC."); + } + + boolean v6Plumbed = ipv6 != null && LoopBackInterface.isIpPlumbed(ipv6); + if (ipv6 != null && !v6Plumbed) { + LOG.log( + Level.FINE, + "Direct Access investigation: IPv6 assigned by metadata but not found on NIC."); + } + + // If the metadata server assigned IPs, but the guest OS hasn't configured any of them on an + // interface. + // Do NOT return early here, because this is how GKE pods work (relying on default kernel + // routing). + if (!v4Plumbed && !v6Plumbed) { + LOG.log( + Level.FINE, + "Direct Access investigation: Metadata IPs are not plumbed to local interfaces (likely containerized). Relying on kernel default routing."); + } + recordAndLog( + tracer, + REASON_UNKNOWN, + "Direct Access investigation: Running on GCP, metadata reachable, IPs assigned and plumbed, but Direct Access still failed.", + originalError); + + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to complete Direct Access investigation", e); + } + } + + /** Helper method to consistently log the failure reason and record it to the tracer. */ + private static void recordAndLog( + DirectPathCompatibleTracer tracer, String reasonCode, String logMessage, Throwable error) { + if (error != null) { + LOG.log(Level.FINE, logMessage, error); + } else { + LOG.log(Level.FINE, logMessage); + } + tracer.recordFailure(reasonCode); + } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java index 49c315a7de..051756144c 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java @@ -1,38 +1,55 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.cloud.bigtable.data.v2.internal.dp; +import com.google.api.core.InternalApi; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +@InternalApi class GCECheck { - private static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google"; - private static final String GCE_PRODUCTION_NAME_AFTER_2016 = "Google Compute Engine"; + private static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google"; + private static final String GCE_PRODUCTION_NAME_AFTER_2016 = "Google Compute Engine"; - @VisibleForTesting - static String systemProductName = null; + @VisibleForTesting static String systemProductName = null; - static boolean isRunningOnGCP() { - String osName = System.getProperty("os.name"); - if ("Linux".equals(osName)) { - String productName = getSystemProductName(); - return productName.contains(GCE_PRODUCTION_NAME_PRIOR_2016) - || productName.contains(GCE_PRODUCTION_NAME_AFTER_2016); - } - return false; + static boolean isRunningOnGCP() { + String osName = System.getProperty("os.name"); + if ("Linux".equals(osName)) { + String productName = getSystemProductName(); + return productName.contains(GCE_PRODUCTION_NAME_PRIOR_2016) + || productName.contains(GCE_PRODUCTION_NAME_AFTER_2016); } + return false; + } - private static String getSystemProductName() { - if (systemProductName != null) { - return systemProductName; - } - try { - return new String( - Files.readAllBytes(Paths.get("/sys/class/dmi/id/product_name")), - StandardCharsets.UTF_8).trim(); - } catch (IOException e) { - return ""; - } + private static String getSystemProductName() { + if (systemProductName != null) { + return systemProductName; } -} \ No newline at end of file + try { + return new String( + Files.readAllBytes(Paths.get("/sys/class/dmi/id/product_name")), + StandardCharsets.UTF_8) + .trim(); + } catch (IOException e) { + return ""; + } + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java index 04b4ef7eb5..503283ee38 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java @@ -1,4 +1,88 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.cloud.bigtable.data.v2.internal.dp; -public class LoopbackInterface { +import com.google.api.core.InternalApi; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; + +/** + * This class verifies two main things: The OS has a functioning loopback interface (lo) with + * standard localhost IPs configured. + */ +@InternalApi +class LoopBackInterface { + + static boolean isUp() throws Exception { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + if (iface.isLoopback() && iface.isUp()) { + return true; + } + } + return false; + } + + /** + * Verifies that the standard IPv4 localhost address (127.0.0.1) is bound to a loopback interface. + */ + static boolean hasLocalIpv4Loopback() throws Exception { + return checkLocalLoopbackAddress("127.0.0.1"); + } + + /** Verifies that the standard IPv6 localhost address (::1) is bound to a loopback interface. */ + static boolean hasLocalIpv6Loopback() throws Exception { + return checkLocalLoopbackAddress("::1"); + } + + static boolean isIpPlumbed(InetAddress expectedIp) throws Exception { + if (expectedIp == null) { + return false; + } + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + if (!iface.isLoopback() && iface.isUp()) { + Enumeration addrs = iface.getInetAddresses(); + while (addrs.hasMoreElements()) { + if (addrs.nextElement().equals(expectedIp)) { + return true; + } + } + } + } + return false; + } + + private static boolean checkLocalLoopbackAddress(String expectedIp) throws Exception { + InetAddress expected = InetAddress.getByName(expectedIp); + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + if (iface.isLoopback() && iface.isUp()) { + Enumeration addrs = iface.getInetAddresses(); + while (addrs.hasMoreElements()) { + if (addrs.nextElement().equals(expected)) { + return true; + } + } + } + } + return false; + } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java index f2cd76867d..528103137c 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java @@ -1,4 +1,98 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.cloud.bigtable.data.v2.internal.dp; -public class MetadataServer { +import com.google.api.core.InternalApi; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +/** + * Verifies that the VM can reach the GCP metadata server and checks whether GCP has successfully + * assigned DirectPath-eligible IPv4 or IPv6 addresses to the instance's primary network interface + * (nic0). + */ +@InternalApi +class MetadataServer { + private static final String METADATA_BASE_URL = + "http://metadata.google.internal/computeMetadata/v1/"; + private static final String METADATA_IPV4_URL = + "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip"; + private static final String METADATA_IPV6_URL = + "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ipv6s"; + + private static final int REACHABILITY_TIMEOUT_MS = 2000; + private static final int FETCH_IP_TIMEOUT_MS = 5000; + + static boolean isReachable() { + HttpURLConnection conn = null; + try { + conn = createConnection(METADATA_BASE_URL, REACHABILITY_TIMEOUT_MS, REACHABILITY_TIMEOUT_MS); + return conn.getResponseCode() == HttpURLConnection.HTTP_OK; + } catch (Exception e) { + return false; + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + static InetAddress getIPv4() { + return fetchIP(METADATA_IPV4_URL); + } + + static InetAddress getIPv6() { + return fetchIP(METADATA_IPV6_URL); + } + + private static InetAddress fetchIP(String urlStr) { + HttpURLConnection conn = null; + try { + conn = createConnection(urlStr, REACHABILITY_TIMEOUT_MS, FETCH_IP_TIMEOUT_MS); + if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { + try (BufferedReader br = + new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + String ipStr = br.readLine(); + if (ipStr != null && !ipStr.isEmpty()) { + return InetAddress.getByName(ipStr.trim()); + } + } + } + } catch (Exception e) { + // investigator handles the exceptio + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return null; + } + + /** Helper to consistently configure the HttpURLConnection for the GCE Metadata Server. */ + private static HttpURLConnection createConnection( + String urlStr, int connectTimeout, int readTimeout) throws Exception { + HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); + conn.setConnectTimeout(connectTimeout); + conn.setReadTimeout(readTimeout); + conn.setRequestProperty("Metadata-Flavor", "Google"); + return conn; + } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java index 7eb39ea8ef..b9c24e64ee 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java @@ -32,6 +32,7 @@ import com.google.cloud.bigtable.data.v2.internal.csm.Metrics; import com.google.cloud.bigtable.data.v2.internal.csm.MetricsImpl; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; import com.google.cloud.bigtable.data.v2.stub.metrics.CustomOpenTelemetryMetricsProvider; import com.google.cloud.bigtable.gaxx.grpc.BigtableTransportChannelProvider; import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer; @@ -45,6 +46,7 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.Optional; import java.util.concurrent.ScheduledExecutorService; import java.util.logging.Level; import java.util.logging.Logger; @@ -160,14 +162,19 @@ public static BigtableClientContext create( credentials, builder.getHeaderProvider().getHeaders()); } + + Optional optionalTracer = + settings.isDirectPathEnabledByDefault() + ? Optional.ofNullable(metrics.getDirectPathCompatibleTracer()) + : Optional.empty(); + BigtableTransportChannelProvider btTransportProvider = BigtableTransportChannelProvider.create( transportProvider.build(), channelPrimer, metrics.getChannelPoolMetricsTracer(), backgroundExecutor, - metrics.getDirectPathCompatibleTracer(), - builder.isDirectPathEnabledByDefault()); + optionalTracer); builder.setTransportChannelProvider(btTransportProvider); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index c54e05db3e..3e36fb4f59 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -60,6 +60,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.threeten.bp.Duration; @@ -100,6 +101,13 @@ public class EnhancedBigtableStubSettings extends StubSettings KEY = CallOptions.Key.create("bigtable-sideband"); @@ -116,7 +111,7 @@ public static SidebandData from(CallOptions callOptions) { @Nullable private volatile ResponseParams responseParams; @Nullable private volatile PeerInfo peerInfo; @Nullable private volatile Duration gfeTiming; - @Nullable private volatile IpProtocol ipProtocol; + @Nullable private volatile Util.IpProtocol ipProtocol; @Nullable public ResponseParams getResponseParams() { @@ -134,7 +129,7 @@ public Duration getGfeTiming() { } @Nullable - public IpProtocol getIpProtocol() { + public Util.IpProtocol getIpProtocol() { return ipProtocol; } @@ -142,7 +137,7 @@ private void reset() { responseParams = null; peerInfo = null; gfeTiming = null; - ipProtocol = IpProtocol.UNKNOWN; + ipProtocol = Util.IpProtocol.UNKNOWN; } void onResponseHeaders(Metadata md, Attributes attributes) { @@ -159,17 +154,17 @@ void onClose(Status status, Metadata trailers) { } @Nullable - private static IpProtocol extractIpProtocol(Attributes attributes) { + private static Util.IpProtocol extractIpProtocol(Attributes attributes) { SocketAddress remoteAddr = attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); if (remoteAddr instanceof InetSocketAddress) { InetSocketAddress inetAddr = (InetSocketAddress) remoteAddr; if (inetAddr.getAddress() instanceof Inet4Address) { - return IpProtocol.IPV4; + return Util.IpProtocol.IPV4; } else if (inetAddr.getAddress() instanceof Inet6Address) { - return IpProtocol.IPV6; + return Util.IpProtocol.IPV6; } } - return IpProtocol.UNKNOWN; + return Util.IpProtocol.UNKNOWN; } @Nullable diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java index 5915947d68..5a268e62e5 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java @@ -26,11 +26,13 @@ import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; import com.google.cloud.bigtable.data.v2.internal.dp.ClassicDirectAccessChecker; import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessChecker; +import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessInvestigator; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStubSettings; import com.google.common.base.Preconditions; import io.grpc.ManagedChannel; import java.io.IOException; import java.util.Map; +import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Supplier; @@ -50,22 +52,20 @@ public final class BigtableTransportChannelProvider implements TransportChannelP private final ChannelPrimer channelPrimer; @Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer; @Nullable private final ScheduledExecutorService backgroundExecutor; - DirectPathCompatibleTracer directPathCompatibleTracer; - private final boolean enableDirectAccessChecker; + private final Optional directPathCompatibleTracer; private BigtableTransportChannelProvider( InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, ChannelPrimer channelPrimer, @Nullable ChannelPoolMetricsTracer channelPoolMetricsTracer, @Nullable ScheduledExecutorService backgroundExecutor, - DirectPathCompatibleTracer directPathCompatibleTracer, - boolean enableDirectAccessChecker) { + Optional directPathCompatibleTracer) { delegate = Preconditions.checkNotNull(instantiatingGrpcChannelProvider); this.channelPrimer = channelPrimer; this.channelPoolMetricsTracer = channelPoolMetricsTracer; this.backgroundExecutor = backgroundExecutor; - this.directPathCompatibleTracer = directPathCompatibleTracer; - this.enableDirectAccessChecker = enableDirectAccessChecker; + this.directPathCompatibleTracer = + directPathCompatibleTracer != null ? directPathCompatibleTracer : Optional.empty(); } @Override @@ -96,8 +96,7 @@ public BigtableTransportChannelProvider withExecutor(Executor executor) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer, - enableDirectAccessChecker); + directPathCompatibleTracer); } @Override @@ -114,8 +113,7 @@ public TransportChannelProvider withBackgroundExecutor(ScheduledExecutorService channelPrimer, channelPoolMetricsTracer, executor, - directPathCompatibleTracer, - enableDirectAccessChecker); + directPathCompatibleTracer); } @Override @@ -132,8 +130,7 @@ public BigtableTransportChannelProvider withHeaders(Map headers) channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer, - enableDirectAccessChecker); + directPathCompatibleTracer); } @Override @@ -150,8 +147,7 @@ public TransportChannelProvider withEndpoint(String endpoint) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer, - enableDirectAccessChecker); + directPathCompatibleTracer); } @Deprecated @@ -170,8 +166,7 @@ public TransportChannelProvider withPoolSize(int size) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer, - enableDirectAccessChecker); + directPathCompatibleTracer); } /** Expected to only be called once when BigtableClientContext is created */ @@ -182,31 +177,35 @@ public TransportChannel getTransportChannel() throws IOException { .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) .build(); - DirectAccessChecker directAccessChecker = - new ClassicDirectAccessChecker(directPathCompatibleTracer, channelPrimer); boolean isDirectAccessEligible = false; - if (enableDirectAccessChecker) { - Supplier maybeDirectAccessChannelSupplier = - () -> { - try { - GrpcTransportChannel channel = - (GrpcTransportChannel) directAccessProvider.getTransportChannel(); - return (ManagedChannel) channel.getChannel(); - } catch (IOException e) { - throw new java.io.UncheckedIOException(e); - } - }; - + if (!directPathCompatibleTracer.isPresent()) { + LOG.fine("Direct access check skipped. Reason: user_disabled or tracer absent"); + LOG.fine("Direct access check skipped. Reason: user_disabled"); + } else { + DirectPathCompatibleTracer tracer = directPathCompatibleTracer.get(); + DirectAccessChecker directAccessChecker = + new ClassicDirectAccessChecker(tracer, channelPrimer); try { - isDirectAccessEligible = directAccessChecker.check(maybeDirectAccessChannelSupplier); + GrpcTransportChannel grpcTransportChannel = + (GrpcTransportChannel) directAccessProvider.getTransportChannel(); + ManagedChannel directAccessChannel = (ManagedChannel) grpcTransportChannel.getChannel(); + + isDirectAccessEligible = directAccessChecker.check(directAccessChannel); + if (!isDirectAccessEligible && backgroundExecutor != null) { + backgroundExecutor.execute( + () -> DirectAccessInvestigator.investigateAndReport(tracer, null)); + } } catch (Exception e) { LOG.log(Level.FINE, "Client is not direct access eligible, using standard transport.", e); + if (backgroundExecutor != null) { + backgroundExecutor.execute( + () -> DirectAccessInvestigator.investigateAndReport(tracer, e)); + } } } InstantiatingGrpcChannelProvider selectedProvider; - if (isDirectAccessEligible) { selectedProvider = directAccessProvider; } else { @@ -270,8 +269,7 @@ public TransportChannelProvider withCredentials(Credentials credentials) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer, - enableDirectAccessChecker); + directPathCompatibleTracer); } /** Creates a BigtableTransportChannelProvider. */ @@ -280,14 +278,12 @@ public static BigtableTransportChannelProvider create( ChannelPrimer channelPrimer, ChannelPoolMetricsTracer outstandingRpcsMetricTracker, ScheduledExecutorService backgroundExecutor, - DirectPathCompatibleTracer directPathCompatibleTracer, - boolean enableDirectAccessChecker) { + Optional directPathCompatibleTracer) { return new BigtableTransportChannelProvider( instantiatingGrpcChannelProvider, channelPrimer, outstandingRpcsMetricTracker, backgroundExecutor, - directPathCompatibleTracer, - enableDirectAccessChecker); + directPathCompatibleTracer); } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java index f6c6e27915..781299a204 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java @@ -20,12 +20,12 @@ import static org.mockito.Mockito.*; import com.google.bigtable.v2.PeerInfo; +import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor; import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer; import io.grpc.Channel; import io.grpc.ManagedChannel; -import java.util.function.Supplier; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -41,9 +41,8 @@ public class ClassicDirectAccessCheckerTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @Mock private ChannelPrimer mockChannelPrimer; - @Mock private Supplier mockChannelFactory; - @Mock private DirectPathCompatibleTracer mockTracer; @Mock private ManagedChannel mockChannel; + @Mock private DirectPathCompatibleTracer mockTracer; @Mock private MetadataExtractorInterceptor mockInterceptor; @Mock private MetadataExtractorInterceptor.SidebandData mockSidebandData; @@ -53,8 +52,6 @@ public class ClassicDirectAccessCheckerTest { public void setUp() throws Exception { checker = spy(new ClassicDirectAccessChecker(mockTracer, mockChannelPrimer)); doReturn(mockInterceptor).when(checker).createInterceptor(); - - when(mockChannelFactory.get()).thenReturn(mockChannel); when(mockInterceptor.getSidebandData()).thenReturn(mockSidebandData); } @@ -65,26 +62,25 @@ public void testEligibleForDirectAccess() { .setTransportType(PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) .build(); when(mockSidebandData.getPeerInfo()).thenReturn(peerInfo); - when(mockSidebandData.getIpProtocol()) - .thenReturn(MetadataExtractorInterceptor.SidebandData.IpProtocol.IPV6); + when(mockSidebandData.getIpProtocol()).thenReturn(Util.IpProtocol.IPV6); - boolean isEligible = checker.check(mockChannelFactory); + boolean isEligible = checker.check(mockChannel); assertThat(isEligible).isTrue(); verify(mockChannelPrimer).primeChannel(any(Channel.class)); - verify(mockTracer).recordSuccess("ipv6"); + verify(mockTracer).recordSuccess(Util.IpProtocol.IPV6); verify(mockChannel).shutdownNow(); } @Test - public void testNotEligibleProxiedRouting() { + public void testNotEligibleCFE() { PeerInfo peerInfo = PeerInfo.newBuilder() .setTransportType(PeerInfo.TransportType.TRANSPORT_TYPE_CLOUD_PATH) .build(); when(mockSidebandData.getPeerInfo()).thenReturn(peerInfo); - boolean isEligible = checker.check(mockChannelFactory); + boolean isEligible = checker.check(mockChannel); assertThat(isEligible).isFalse(); verifyNoInteractions(mockTracer); @@ -96,7 +92,7 @@ public void testMissingSidebandData() { // Override the Before setup to return null for this specific test when(mockInterceptor.getSidebandData()).thenReturn(null); - boolean isEligible = checker.check(mockChannelFactory); + boolean isEligible = checker.check(mockChannel); assertThat(isEligible).isFalse(); verifyNoInteractions(mockTracer); @@ -109,8 +105,19 @@ public void testExceptionSafetyAndCleanup() { .when(mockChannelPrimer) .primeChannel(any(Channel.class)); - boolean isEligible = checker.check(mockChannelFactory); + boolean isEligible = checker.check(mockChannel); + + assertThat(isEligible).isFalse(); + verifyNoInteractions(mockTracer); + verify(mockChannel).shutdownNow(); + } + + @Test + public void testNullPeerInfoIsHandledSafely() { + when(mockInterceptor.getSidebandData()).thenReturn(mockSidebandData); + when(mockSidebandData.getPeerInfo()).thenReturn(null); + boolean isEligible = checker.check(mockChannel); assertThat(isEligible).isFalse(); verifyNoInteractions(mockTracer); verify(mockChannel).shutdownNow(); From afc7d44b471f3f738183f1829c9b84290694aaf7 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Sun, 29 Mar 2026 16:39:54 -0400 Subject: [PATCH 18/28] fix --- .../dp/ClassicDirectAccessChecker.java | 14 ++++----- .../v2/stub/EnhancedBigtableStubSettings.java | 29 +++++++++---------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java index dfb3bf029a..951a74cda8 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java @@ -64,9 +64,7 @@ public boolean check(Channel channel) { } } - /** - * Executes the underlying RPC and evaluates the eligibility. - */ + /** Executes the underlying RPC and evaluates the eligibility. */ private boolean evaluateEligibility(Channel channel) { MetadataExtractorInterceptor interceptor = createInterceptor(); Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor); @@ -74,11 +72,11 @@ private boolean evaluateEligibility(Channel channel) { MetadataExtractorInterceptor.SidebandData sidebandData = interceptor.getSidebandData(); boolean isEligible = - Optional.ofNullable(sidebandData) - .map(MetadataExtractorInterceptor.SidebandData::getPeerInfo) - .map(PeerInfo::getTransportType) - .map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) - .orElse(false); + Optional.ofNullable(sidebandData) + .map(MetadataExtractorInterceptor.SidebandData::getPeerInfo) + .map(PeerInfo::getTransportType) + .map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS) + .orElse(false); if (isEligible) { // getIp should be non-null as isEligible is true diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index 3e36fb4f59..9312bfe143 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -252,25 +252,24 @@ public ClientOperationSettings getPerOpSettings() { return perOpSettings; } - /** Returns a builder for the default ChannelProvider for this service. */ public static InstantiatingGrpcChannelProvider.Builder defaultGrpcTransportProviderBuilder() { InstantiatingGrpcChannelProvider.Builder grpcTransportProviderBuilder = - BigtableStubSettings.defaultGrpcTransportProviderBuilder(); + BigtableStubSettings.defaultGrpcTransportProviderBuilder(); return grpcTransportProviderBuilder - .setChannelPoolSettings( - ChannelPoolSettings.builder() - .setInitialChannelCount(10) - .setMinRpcsPerChannel(1) - // Keep it conservative as we scale the channel size every 1min - // and delta is 2 channels. - .setMaxRpcsPerChannel(25) - .setPreemptiveRefreshEnabled(true) - .build()) - .setMaxInboundMessageSize(MAX_MESSAGE_SIZE) - .setKeepAliveTime(Duration.ofSeconds(30)) // sends ping in this interval - .setKeepAliveTimeout( - Duration.ofSeconds(10)); // wait this long before considering the connection dead + .setChannelPoolSettings( + ChannelPoolSettings.builder() + .setInitialChannelCount(10) + .setMinRpcsPerChannel(1) + // Keep it conservative as we scale the channel size every 1min + // and delta is 2 channels. + .setMaxRpcsPerChannel(25) + .setPreemptiveRefreshEnabled(true) + .build()) + .setMaxInboundMessageSize(MAX_MESSAGE_SIZE) + .setKeepAliveTime(Duration.ofSeconds(30)) // sends ping in this interval + .setKeepAliveTimeout( + Duration.ofSeconds(10)); // wait this long before considering the connection dead } /** Applies Direct Access traits to an existing builder. */ From bea4f02bd725b6c26dc2706296cc89a14c274c73 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Sun, 29 Mar 2026 16:41:01 -0400 Subject: [PATCH 19/28] fix --- .../bigtable/gaxx/grpc/BigtableTransportChannelProvider.java | 1 - 1 file changed, 1 deletion(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java index 5a268e62e5..2baeb3ca3c 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java @@ -181,7 +181,6 @@ public TransportChannel getTransportChannel() throws IOException { if (!directPathCompatibleTracer.isPresent()) { LOG.fine("Direct access check skipped. Reason: user_disabled or tracer absent"); - LOG.fine("Direct access check skipped. Reason: user_disabled"); } else { DirectPathCompatibleTracer tracer = directPathCompatibleTracer.get(); DirectAccessChecker directAccessChecker = From 72903067b3414d5eb90032775d10cc30b382ba85 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Mon, 30 Mar 2026 11:38:12 -0400 Subject: [PATCH 20/28] fix --- .../internal/dp/DirectAccessInvestigator.java | 120 ++++-------------- .../data/v2/internal/dp/GCECheck.java | 55 -------- .../v2/internal/dp/LoopBackInterface.java | 88 ------------- .../data/v2/internal/dp/MetadataServer.java | 98 -------------- 4 files changed, 27 insertions(+), 334 deletions(-) delete mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java delete mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java delete mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java index b32ace922c..f2d5f52dc6 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java @@ -18,7 +18,6 @@ import com.google.api.core.InternalApi; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; -import java.net.InetAddress; import java.util.logging.Level; import java.util.logging.Logger; @@ -26,103 +25,38 @@ public class DirectAccessInvestigator { private static final Logger LOG = Logger.getLogger(DirectAccessInvestigator.class.getName()); - // Telemetry metric reason codes - private static final String REASON_NOT_IN_GCP = "not_in_gcp"; - private static final String REASON_METADATA_UNREACHABLE = "metadata_unreachable"; - private static final String REASON_NO_IP_ASSIGNED = "no_ip_assigned"; - private static final String REASON_LOOPBACK_DOWN = "loopback_misconfigured"; - private static final String REASON_LOOPBACK_V4_MISSING = "loopback_misconfigured_ipv4"; - private static final String REASON_LOOPBACK_V6_MISSING = "loopback_misconfigured_ipv6"; - private static final String REASON_UNKNOWN = ""; + /** Metric reason codes for Direct Access failures. */ + public enum FailureReason { + NOT_IN_GCP("not_in_gcp"), + METADATA_UNREACHABLE("metadata_unreachable"), + NO_IP_ASSIGNED("no_ip_assigned"), + LOOPBACK_DOWN("loopback_misconfigured"), + LOOPBACK_V4_MISSING("loopback_misconfigured_ipv4"), + LOOPBACK_V6_MISSING("loopback_misconfigured_ipv6"), + USER_DISABLED("user_disabled"), + UNKNOWN(""); + + private final String value; + + FailureReason(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } public static void investigateAndReport( DirectPathCompatibleTracer tracer, Throwable originalError) { try { - if (!GCECheck.isRunningOnGCP()) { - recordAndLog( - tracer, REASON_NOT_IN_GCP, "Direct Access investigation: not in GCP.", originalError); - return; - } - - if (!MetadataServer.isReachable()) { - recordAndLog( - tracer, - REASON_METADATA_UNREACHABLE, - "Direct Access investigation: Metadata unreachable.", - null); - return; - } - - InetAddress ipv4 = MetadataServer.getIPv4(); - InetAddress ipv6 = MetadataServer.getIPv6(); - - if (ipv4 == null && ipv6 == null) { - recordAndLog( - tracer, - REASON_NO_IP_ASSIGNED, - "Direct Access investigation: Neither IPv4 nor IPv6 assigned.", - null); - return; - } - - if (!LoopBackInterface.isUp()) { - recordAndLog( - tracer, - REASON_LOOPBACK_DOWN, - "Direct Access investigation: Loopback interface down.", - null); - return; - } - - // ONLY check for IPv4 loopback if we are using an IPv4 address - if (ipv4 != null && !LoopBackInterface.hasLocalIpv4Loopback()) { - recordAndLog( - tracer, - REASON_LOOPBACK_V4_MISSING, - "Direct Access investigation: IPv4 loopback missing.", - null); - return; - } - - // ONLY check for IPv6 loopback if we are using an IPv6 address - if (ipv6 != null && !LoopBackInterface.hasLocalIpv6Loopback()) { - recordAndLog( - tracer, - REASON_LOOPBACK_V6_MISSING, - "Direct Access investigation: IPv6 loopback missing.", - null); - return; - } - - boolean v4Plumbed = ipv4 != null && LoopBackInterface.isIpPlumbed(ipv4); - if (ipv4 != null && !v4Plumbed) { - LOG.log( - Level.FINE, - "Direct Access investigation: IPv4 assigned by metadata but not found on NIC."); - } - - boolean v6Plumbed = ipv6 != null && LoopBackInterface.isIpPlumbed(ipv6); - if (ipv6 != null && !v6Plumbed) { - LOG.log( - Level.FINE, - "Direct Access investigation: IPv6 assigned by metadata but not found on NIC."); - } - - // If the metadata server assigned IPs, but the guest OS hasn't configured any of them on an - // interface. - // Do NOT return early here, because this is how GKE pods work (relying on default kernel - // routing). - if (!v4Plumbed && !v6Plumbed) { - LOG.log( - Level.FINE, - "Direct Access investigation: Metadata IPs are not plumbed to local interfaces (likely containerized). Relying on kernel default routing."); - } + // TODO: Implement checks in a future PR. + // For now, default to returning "unknown". recordAndLog( tracer, - REASON_UNKNOWN, - "Direct Access investigation: Running on GCP, metadata reachable, IPs assigned and plumbed, but Direct Access still failed.", + FailureReason.UNKNOWN, + "Direct Access investigation: Defaulting to unknown failure reason for now.", originalError); - } catch (Exception e) { LOG.log(Level.WARNING, "Failed to complete Direct Access investigation", e); } @@ -130,12 +64,12 @@ public static void investigateAndReport( /** Helper method to consistently log the failure reason and record it to the tracer. */ private static void recordAndLog( - DirectPathCompatibleTracer tracer, String reasonCode, String logMessage, Throwable error) { + DirectPathCompatibleTracer tracer, FailureReason reason, String logMessage, Throwable error) { if (error != null) { LOG.log(Level.FINE, logMessage, error); } else { LOG.log(Level.FINE, logMessage); } - tracer.recordFailure(reasonCode); + tracer.recordFailure(reason.getValue()); } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java deleted file mode 100644 index 051756144c..0000000000 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/GCECheck.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.cloud.bigtable.data.v2.internal.dp; - -import com.google.api.core.InternalApi; -import com.google.common.annotations.VisibleForTesting; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; - -@InternalApi -class GCECheck { - private static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google"; - private static final String GCE_PRODUCTION_NAME_AFTER_2016 = "Google Compute Engine"; - - @VisibleForTesting static String systemProductName = null; - - static boolean isRunningOnGCP() { - String osName = System.getProperty("os.name"); - if ("Linux".equals(osName)) { - String productName = getSystemProductName(); - return productName.contains(GCE_PRODUCTION_NAME_PRIOR_2016) - || productName.contains(GCE_PRODUCTION_NAME_AFTER_2016); - } - return false; - } - - private static String getSystemProductName() { - if (systemProductName != null) { - return systemProductName; - } - try { - return new String( - Files.readAllBytes(Paths.get("/sys/class/dmi/id/product_name")), - StandardCharsets.UTF_8) - .trim(); - } catch (IOException e) { - return ""; - } - } -} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java deleted file mode 100644 index 503283ee38..0000000000 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/LoopBackInterface.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.cloud.bigtable.data.v2.internal.dp; - -import com.google.api.core.InternalApi; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.util.Enumeration; - -/** - * This class verifies two main things: The OS has a functioning loopback interface (lo) with - * standard localhost IPs configured. - */ -@InternalApi -class LoopBackInterface { - - static boolean isUp() throws Exception { - Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); - while (interfaces.hasMoreElements()) { - NetworkInterface iface = interfaces.nextElement(); - if (iface.isLoopback() && iface.isUp()) { - return true; - } - } - return false; - } - - /** - * Verifies that the standard IPv4 localhost address (127.0.0.1) is bound to a loopback interface. - */ - static boolean hasLocalIpv4Loopback() throws Exception { - return checkLocalLoopbackAddress("127.0.0.1"); - } - - /** Verifies that the standard IPv6 localhost address (::1) is bound to a loopback interface. */ - static boolean hasLocalIpv6Loopback() throws Exception { - return checkLocalLoopbackAddress("::1"); - } - - static boolean isIpPlumbed(InetAddress expectedIp) throws Exception { - if (expectedIp == null) { - return false; - } - Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); - while (interfaces.hasMoreElements()) { - NetworkInterface iface = interfaces.nextElement(); - if (!iface.isLoopback() && iface.isUp()) { - Enumeration addrs = iface.getInetAddresses(); - while (addrs.hasMoreElements()) { - if (addrs.nextElement().equals(expectedIp)) { - return true; - } - } - } - } - return false; - } - - private static boolean checkLocalLoopbackAddress(String expectedIp) throws Exception { - InetAddress expected = InetAddress.getByName(expectedIp); - Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); - while (interfaces.hasMoreElements()) { - NetworkInterface iface = interfaces.nextElement(); - if (iface.isLoopback() && iface.isUp()) { - Enumeration addrs = iface.getInetAddresses(); - while (addrs.hasMoreElements()) { - if (addrs.nextElement().equals(expected)) { - return true; - } - } - } - } - return false; - } -} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java deleted file mode 100644 index 528103137c..0000000000 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/MetadataServer.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.cloud.bigtable.data.v2.internal.dp; - -import com.google.api.core.InternalApi; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.InetAddress; -import java.net.URL; -import java.nio.charset.StandardCharsets; - -/** - * Verifies that the VM can reach the GCP metadata server and checks whether GCP has successfully - * assigned DirectPath-eligible IPv4 or IPv6 addresses to the instance's primary network interface - * (nic0). - */ -@InternalApi -class MetadataServer { - private static final String METADATA_BASE_URL = - "http://metadata.google.internal/computeMetadata/v1/"; - private static final String METADATA_IPV4_URL = - "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip"; - private static final String METADATA_IPV6_URL = - "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ipv6s"; - - private static final int REACHABILITY_TIMEOUT_MS = 2000; - private static final int FETCH_IP_TIMEOUT_MS = 5000; - - static boolean isReachable() { - HttpURLConnection conn = null; - try { - conn = createConnection(METADATA_BASE_URL, REACHABILITY_TIMEOUT_MS, REACHABILITY_TIMEOUT_MS); - return conn.getResponseCode() == HttpURLConnection.HTTP_OK; - } catch (Exception e) { - return false; - } finally { - if (conn != null) { - conn.disconnect(); - } - } - } - - static InetAddress getIPv4() { - return fetchIP(METADATA_IPV4_URL); - } - - static InetAddress getIPv6() { - return fetchIP(METADATA_IPV6_URL); - } - - private static InetAddress fetchIP(String urlStr) { - HttpURLConnection conn = null; - try { - conn = createConnection(urlStr, REACHABILITY_TIMEOUT_MS, FETCH_IP_TIMEOUT_MS); - if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { - try (BufferedReader br = - new BufferedReader( - new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { - String ipStr = br.readLine(); - if (ipStr != null && !ipStr.isEmpty()) { - return InetAddress.getByName(ipStr.trim()); - } - } - } - } catch (Exception e) { - // investigator handles the exceptio - } finally { - if (conn != null) { - conn.disconnect(); - } - } - return null; - } - - /** Helper to consistently configure the HttpURLConnection for the GCE Metadata Server. */ - private static HttpURLConnection createConnection( - String urlStr, int connectTimeout, int readTimeout) throws Exception { - HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); - conn.setConnectTimeout(connectTimeout); - conn.setReadTimeout(readTimeout); - conn.setRequestProperty("Metadata-Flavor", "Google"); - return conn; - } -} From 7f83e59453657b6e0091759c66b361feb2ce7696 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Mon, 30 Mar 2026 12:56:15 -0400 Subject: [PATCH 21/28] fix --- .../internal/dp/DirectAccessInvestigator.java | 1 + .../data/v2/stub/BigtableChannelSupplier.java | 23 ------------------- .../data/v2/stub/BigtableClientContext.java | 2 +- 3 files changed, 2 insertions(+), 24 deletions(-) delete mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelSupplier.java diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java index f2d5f52dc6..9ba3168288 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java @@ -47,6 +47,7 @@ public String getValue() { } } + // This is only called when direct access check fails. public static void investigateAndReport( DirectPathCompatibleTracer tracer, Throwable originalError) { try { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelSupplier.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelSupplier.java deleted file mode 100644 index 24aa062e14..0000000000 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableChannelSupplier.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.cloud.bigtable.data.v2.stub; - -import com.google.api.core.InternalApi; -import io.grpc.ManagedChannel; -import java.util.function.Supplier; - -@InternalApi -public interface BigtableChannelSupplier extends Supplier {} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java index b9c24e64ee..a573d915a3 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java @@ -165,7 +165,7 @@ public static BigtableClientContext create( Optional optionalTracer = settings.isDirectPathEnabledByDefault() - ? Optional.ofNullable(metrics.getDirectPathCompatibleTracer()) + ? Optional.of(metrics.getDirectPathCompatibleTracer()) : Optional.empty(); BigtableTransportChannelProvider btTransportProvider = From c3ab8789d6642c39c73eb5ec8353d05865c74b5b Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Mon, 30 Mar 2026 13:11:09 -0400 Subject: [PATCH 22/28] fix --- .../data/v2/internal/dp/DirectAccessInvestigator.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java index 9ba3168288..6dc03b9db3 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java @@ -20,6 +20,7 @@ import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nullable; @InternalApi public class DirectAccessInvestigator { @@ -34,7 +35,7 @@ public enum FailureReason { LOOPBACK_V4_MISSING("loopback_misconfigured_ipv4"), LOOPBACK_V6_MISSING("loopback_misconfigured_ipv6"), USER_DISABLED("user_disabled"), - UNKNOWN(""); + UNKNOWN("unknown"); private final String value; @@ -49,7 +50,7 @@ public String getValue() { // This is only called when direct access check fails. public static void investigateAndReport( - DirectPathCompatibleTracer tracer, Throwable originalError) { + DirectPathCompatibleTracer tracer, @Nullable Throwable originalError) { try { // TODO: Implement checks in a future PR. // For now, default to returning "unknown". @@ -65,7 +66,10 @@ public static void investigateAndReport( /** Helper method to consistently log the failure reason and record it to the tracer. */ private static void recordAndLog( - DirectPathCompatibleTracer tracer, FailureReason reason, String logMessage, Throwable error) { + DirectPathCompatibleTracer tracer, + FailureReason reason, + String logMessage, + @Nullable Throwable error) { if (error != null) { LOG.log(Level.FINE, logMessage, error); } else { From 4f4b430988e1dcdc73eb797af1511403dce57ea1 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Tue, 31 Mar 2026 11:46:55 -0400 Subject: [PATCH 23/28] fix --- .../dp/ClassicDirectAccessChecker.java | 18 +++++- .../v2/internal/dp/DirectAccessChecker.java | 7 +++ .../internal/dp/NoopDirectAccessChecker.java | 38 ++++++++++++ .../data/v2/stub/BigtableClientContext.java | 19 ++++-- .../BigtableTransportChannelProvider.java | 62 ++++++------------- .../dp/ClassicDirectAccessCheckerTest.java | 36 ++++++++++- 6 files changed, 128 insertions(+), 52 deletions(-) create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/NoopDirectAccessChecker.java diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java index 951a74cda8..64ed857a98 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java @@ -25,8 +25,10 @@ import io.grpc.ClientInterceptors; import io.grpc.ManagedChannel; import java.util.Optional; +import java.util.concurrent.ScheduledExecutorService; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nullable; /** * Evaluates whether a given channel has Direct Access (DirectPath) routing by executing a RPC and @@ -37,11 +39,15 @@ public class ClassicDirectAccessChecker implements DirectAccessChecker { private static final Logger LOG = Logger.getLogger(ClassicDirectAccessChecker.class.getName()); private final DirectPathCompatibleTracer tracer; private final ChannelPrimer channelPrimer; + private final ScheduledExecutorService executor; public ClassicDirectAccessChecker( - DirectPathCompatibleTracer tracer, ChannelPrimer channelPrimer) { + DirectPathCompatibleTracer tracer, + ChannelPrimer channelPrimer, + ScheduledExecutorService executor) { this.tracer = tracer; this.channelPrimer = channelPrimer; + this.executor = executor; } @VisibleForTesting @@ -54,6 +60,7 @@ public boolean check(Channel channel) { try { return evaluateEligibility(channel); } catch (Exception e) { + investigateFailure(e); LOG.log(Level.WARNING, "Failed to evaluate direct access eligibility.", e); return false; } finally { @@ -81,7 +88,16 @@ private boolean evaluateEligibility(Channel channel) { if (isEligible) { // getIp should be non-null as isEligible is true tracer.recordSuccess(sidebandData.getIpProtocol()); + } else { + investigateFailure(null); } return isEligible; } + + @Override + public void investigateFailure(@Nullable Throwable originalError) { + if (executor != null) { + executor.execute(() -> DirectAccessInvestigator.investigateAndReport(tracer, originalError)); + } + } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java index 2f75f6503f..e6954d279d 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java @@ -28,4 +28,11 @@ public interface DirectAccessChecker { * @return true if the channel is eligible for Direct Access */ boolean check(Channel channel); + + /** + * Triggers a investigation into why Direct Access routing failed. + * + * @param originalError An optional exception that caused the failure. + */ + void investigateFailure(Throwable originalError); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/NoopDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/NoopDirectAccessChecker.java new file mode 100644 index 0000000000..cf727996ac --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/NoopDirectAccessChecker.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.data.v2.internal.dp; + +import com.google.api.core.InternalApi; +import io.grpc.Channel; +import javax.annotation.Nullable; + +@InternalApi +public class NoopDirectAccessChecker implements DirectAccessChecker { + public static final NoopDirectAccessChecker INSTANCE = new NoopDirectAccessChecker(); + + private NoopDirectAccessChecker() {} + + @Override + public boolean check(Channel channel) { + // If it's disabled, it is never eligible. + return false; + } + + @Override + public void investigateFailure(@Nullable Throwable originalError) { + // Do nothing. We don't investigate failures if the feature is disabled. + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java index a573d915a3..a6d96eb91c 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java @@ -33,6 +33,10 @@ import com.google.cloud.bigtable.data.v2.internal.csm.MetricsImpl; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.NoopDirectPathCompatibleTracer; +import com.google.cloud.bigtable.data.v2.internal.dp.ClassicDirectAccessChecker; +import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessChecker; +import com.google.cloud.bigtable.data.v2.internal.dp.NoopDirectAccessChecker; import com.google.cloud.bigtable.data.v2.stub.metrics.CustomOpenTelemetryMetricsProvider; import com.google.cloud.bigtable.gaxx.grpc.BigtableTransportChannelProvider; import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer; @@ -46,7 +50,6 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.util.Optional; import java.util.concurrent.ScheduledExecutorService; import java.util.logging.Level; import java.util.logging.Logger; @@ -163,10 +166,16 @@ public static BigtableClientContext create( builder.getHeaderProvider().getHeaders()); } - Optional optionalTracer = + DirectPathCompatibleTracer directPathCompatibleTracer = settings.isDirectPathEnabledByDefault() - ? Optional.of(metrics.getDirectPathCompatibleTracer()) - : Optional.empty(); + ? metrics.getDirectPathCompatibleTracer() + : NoopDirectPathCompatibleTracer.INSTANCE; + + DirectAccessChecker directAccessChecker = + settings.isDirectPathEnabledByDefault() + ? new ClassicDirectAccessChecker( + directPathCompatibleTracer, channelPrimer, backgroundExecutor) + : NoopDirectAccessChecker.INSTANCE; BigtableTransportChannelProvider btTransportProvider = BigtableTransportChannelProvider.create( @@ -174,7 +183,7 @@ public static BigtableClientContext create( channelPrimer, metrics.getChannelPoolMetricsTracer(), backgroundExecutor, - optionalTracer); + directAccessChecker); builder.setTransportChannelProvider(btTransportProvider); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java index 2baeb3ca3c..e9521bf193 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java @@ -23,16 +23,12 @@ import com.google.api.gax.rpc.TransportChannelProvider; import com.google.auth.Credentials; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.ChannelPoolMetricsTracer; -import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; -import com.google.cloud.bigtable.data.v2.internal.dp.ClassicDirectAccessChecker; import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessChecker; -import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessInvestigator; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStubSettings; import com.google.common.base.Preconditions; import io.grpc.ManagedChannel; import java.io.IOException; import java.util.Map; -import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Supplier; @@ -52,20 +48,19 @@ public final class BigtableTransportChannelProvider implements TransportChannelP private final ChannelPrimer channelPrimer; @Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer; @Nullable private final ScheduledExecutorService backgroundExecutor; - private final Optional directPathCompatibleTracer; + private final DirectAccessChecker directAccessChecker; private BigtableTransportChannelProvider( InstantiatingGrpcChannelProvider instantiatingGrpcChannelProvider, ChannelPrimer channelPrimer, @Nullable ChannelPoolMetricsTracer channelPoolMetricsTracer, @Nullable ScheduledExecutorService backgroundExecutor, - Optional directPathCompatibleTracer) { + DirectAccessChecker directAccessChecker) { delegate = Preconditions.checkNotNull(instantiatingGrpcChannelProvider); this.channelPrimer = channelPrimer; this.channelPoolMetricsTracer = channelPoolMetricsTracer; this.backgroundExecutor = backgroundExecutor; - this.directPathCompatibleTracer = - directPathCompatibleTracer != null ? directPathCompatibleTracer : Optional.empty(); + this.directAccessChecker = directAccessChecker; } @Override @@ -96,7 +91,7 @@ public BigtableTransportChannelProvider withExecutor(Executor executor) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer); + directAccessChecker); } @Override @@ -109,11 +104,7 @@ public TransportChannelProvider withBackgroundExecutor(ScheduledExecutorService InstantiatingGrpcChannelProvider newChannelProvider = (InstantiatingGrpcChannelProvider) delegate.withBackgroundExecutor(executor); return new BigtableTransportChannelProvider( - newChannelProvider, - channelPrimer, - channelPoolMetricsTracer, - executor, - directPathCompatibleTracer); + newChannelProvider, channelPrimer, channelPoolMetricsTracer, executor, directAccessChecker); } @Override @@ -130,7 +121,7 @@ public BigtableTransportChannelProvider withHeaders(Map headers) channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer); + directAccessChecker); } @Override @@ -147,7 +138,7 @@ public TransportChannelProvider withEndpoint(String endpoint) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer); + directAccessChecker); } @Deprecated @@ -166,7 +157,7 @@ public TransportChannelProvider withPoolSize(int size) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer); + directAccessChecker); } /** Expected to only be called once when BigtableClientContext is created */ @@ -179,29 +170,14 @@ public TransportChannel getTransportChannel() throws IOException { boolean isDirectAccessEligible = false; - if (!directPathCompatibleTracer.isPresent()) { - LOG.fine("Direct access check skipped. Reason: user_disabled or tracer absent"); - } else { - DirectPathCompatibleTracer tracer = directPathCompatibleTracer.get(); - DirectAccessChecker directAccessChecker = - new ClassicDirectAccessChecker(tracer, channelPrimer); - try { - GrpcTransportChannel grpcTransportChannel = - (GrpcTransportChannel) directAccessProvider.getTransportChannel(); - ManagedChannel directAccessChannel = (ManagedChannel) grpcTransportChannel.getChannel(); - - isDirectAccessEligible = directAccessChecker.check(directAccessChannel); - if (!isDirectAccessEligible && backgroundExecutor != null) { - backgroundExecutor.execute( - () -> DirectAccessInvestigator.investigateAndReport(tracer, null)); - } - } catch (Exception e) { - LOG.log(Level.FINE, "Client is not direct access eligible, using standard transport.", e); - if (backgroundExecutor != null) { - backgroundExecutor.execute( - () -> DirectAccessInvestigator.investigateAndReport(tracer, e)); - } - } + try { + GrpcTransportChannel grpcTransportChannel = + (GrpcTransportChannel) directAccessProvider.getTransportChannel(); + ManagedChannel directAccessChannel = (ManagedChannel) grpcTransportChannel.getChannel(); + isDirectAccessEligible = directAccessChecker.check(directAccessChannel); + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to check for direct access.", e); + directAccessChecker.investigateFailure(e); } InstantiatingGrpcChannelProvider selectedProvider; @@ -268,7 +244,7 @@ public TransportChannelProvider withCredentials(Credentials credentials) { channelPrimer, channelPoolMetricsTracer, backgroundExecutor, - directPathCompatibleTracer); + directAccessChecker); } /** Creates a BigtableTransportChannelProvider. */ @@ -277,12 +253,12 @@ public static BigtableTransportChannelProvider create( ChannelPrimer channelPrimer, ChannelPoolMetricsTracer outstandingRpcsMetricTracker, ScheduledExecutorService backgroundExecutor, - Optional directPathCompatibleTracer) { + DirectAccessChecker directAccessChecker) { return new BigtableTransportChannelProvider( instantiatingGrpcChannelProvider, channelPrimer, outstandingRpcsMetricTracker, backgroundExecutor, - directPathCompatibleTracer); + directAccessChecker); } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java index 781299a204..33e6a74f9b 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java @@ -17,7 +17,13 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; import com.google.bigtable.v2.PeerInfo; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util; @@ -26,11 +32,13 @@ import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer; import io.grpc.Channel; import io.grpc.ManagedChannel; +import java.util.concurrent.ScheduledExecutorService; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -45,12 +53,14 @@ public class ClassicDirectAccessCheckerTest { @Mock private DirectPathCompatibleTracer mockTracer; @Mock private MetadataExtractorInterceptor mockInterceptor; @Mock private MetadataExtractorInterceptor.SidebandData mockSidebandData; + @Mock private ScheduledExecutorService mockExecutor; private ClassicDirectAccessChecker checker; @Before public void setUp() throws Exception { - checker = spy(new ClassicDirectAccessChecker(mockTracer, mockChannelPrimer)); + // Pass null for the executor by default so background investigations aren't triggered + checker = spy(new ClassicDirectAccessChecker(mockTracer, mockChannelPrimer, null)); doReturn(mockInterceptor).when(checker).createInterceptor(); when(mockInterceptor.getSidebandData()).thenReturn(mockSidebandData); } @@ -83,7 +93,7 @@ public void testNotEligibleCFE() { boolean isEligible = checker.check(mockChannel); assertThat(isEligible).isFalse(); - verifyNoInteractions(mockTracer); + verifyNoInteractions(mockTracer); // No interactions because executor is null verify(mockChannel).shutdownNow(); } @@ -122,4 +132,24 @@ public void testNullPeerInfoIsHandledSafely() { verifyNoInteractions(mockTracer); verify(mockChannel).shutdownNow(); } + + @Test + public void testInvestigationTriggeredOnFailure() { + // Re-instantiate the checker with a mock executor to verify investigation is scheduled + checker = spy(new ClassicDirectAccessChecker(mockTracer, mockChannelPrimer, mockExecutor)); + doReturn(mockInterceptor).when(checker).createInterceptor(); + when(mockInterceptor.getSidebandData()).thenReturn(null); // Force a failure + + boolean isEligible = checker.check(mockChannel); + + assertThat(isEligible).isFalse(); + + // Verify the checker submitted a Runnable task to the background executor + ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mockExecutor).execute(runnableCaptor.capture()); + + // Execute the captured runnable to ensure it safely calls the tracer + runnableCaptor.getValue().run(); + verify(mockTracer).recordFailure(anyString()); + } } From a46b61ae0747f241518b8a3b8d025abdc74252bd Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Tue, 31 Mar 2026 11:48:29 -0400 Subject: [PATCH 24/28] todo --- .../data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java index 7ae71e9694..db8228b709 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java @@ -34,5 +34,6 @@ public interface DirectPathCompatibleTracer { * * @param reason The reason for the failure (e.g., "routing_check_failed"). */ + // TODO: Make this an enum void recordFailure(String reason); } From 0ab3a4cd447e4d945d2c8c774de7e1452aad218e Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Tue, 31 Mar 2026 15:19:51 -0400 Subject: [PATCH 25/28] fix --- .../data/v2/internal/csm/MetricsImpl.java | 8 ++-- ...va => DirectPathCompatibleTracerImpl.java} | 4 +- .../NoopDirectPathCompatibleTracer.java | 38 ------------------- .../internal/dp/DirectAccessInvestigator.java | 2 +- .../data/v2/stub/BigtableClientContext.java | 9 +---- .../v2/stub/metrics/NoopMetricsProvider.java | 21 ++++++++++ 6 files changed, 29 insertions(+), 53 deletions(-) rename google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/{DefaultDirectPathCompatibleTracer.java => DirectPathCompatibleTracerImpl.java} (91%) delete mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java index 26cb73cce8..88771b4df6 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java @@ -31,10 +31,10 @@ import com.google.cloud.bigtable.data.v2.internal.csm.tracers.BuiltinMetricsTracerFactory; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.ChannelPoolMetricsTracer; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.CompositeTracerFactory; -import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DefaultDirectPathCompatibleTracer; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; -import com.google.cloud.bigtable.data.v2.internal.csm.tracers.NoopDirectPathCompatibleTracer; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracerImpl; import com.google.cloud.bigtable.data.v2.internal.csm.tracers.Pacemaker; +import com.google.cloud.bigtable.data.v2.stub.metrics.NoopMetricsProvider; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -99,7 +99,7 @@ public MetricsImpl( this.pacemaker = new Pacemaker(internalRecorder, clientInfo, "background"); this.channelPoolMetricsTracer = new ChannelPoolMetricsTracer(internalRecorder, clientInfo); this.directPathCompatibleTracer = - new DefaultDirectPathCompatibleTracer(clientInfo, internalRecorder); + new DirectPathCompatibleTracerImpl(clientInfo, internalRecorder); this.grpcOtel = GrpcOpenTelemetry.newBuilder() .sdk(internalOtel) @@ -115,7 +115,7 @@ public MetricsImpl( this.grpcOtel = null; this.pacemaker = null; this.channelPoolMetricsTracer = null; - this.directPathCompatibleTracer = NoopDirectPathCompatibleTracer.INSTANCE; + this.directPathCompatibleTracer = NoopMetricsProvider.NoopDirectPathCompatibleTracer.INSTANCE; } if (userOtel != null) { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracerImpl.java similarity index 91% rename from google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java rename to google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracerImpl.java index 335c414cfa..d4a66d3d11 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DefaultDirectPathCompatibleTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracerImpl.java @@ -21,11 +21,11 @@ import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util; @InternalApi -public class DefaultDirectPathCompatibleTracer implements DirectPathCompatibleTracer { +public class DirectPathCompatibleTracerImpl implements DirectPathCompatibleTracer { private final ClientInfo clientInfo; private final MetricRegistry.RecorderRegistry recorder; - public DefaultDirectPathCompatibleTracer( + public DirectPathCompatibleTracerImpl( ClientInfo clientInfo, MetricRegistry.RecorderRegistry recorder) { this.clientInfo = clientInfo; this.recorder = recorder; diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java deleted file mode 100644 index 88cc767fdd..0000000000 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/NoopDirectPathCompatibleTracer.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.cloud.bigtable.data.v2.internal.csm.tracers; - -import com.google.api.core.InternalApi; -import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util; - -@InternalApi -public class NoopDirectPathCompatibleTracer implements DirectPathCompatibleTracer { - - public static final NoopDirectPathCompatibleTracer INSTANCE = - new NoopDirectPathCompatibleTracer(); - - private NoopDirectPathCompatibleTracer() {} - - @Override - public void recordSuccess(Util.IpProtocol ipProtocol) { - // No-op - } - - @Override - public void recordFailure(String reason) { - // No-op - } -} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java index 6dc03b9db3..32bcb41203 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java @@ -60,7 +60,7 @@ public static void investigateAndReport( "Direct Access investigation: Defaulting to unknown failure reason for now.", originalError); } catch (Exception e) { - LOG.log(Level.WARNING, "Failed to complete Direct Access investigation", e); + LOG.log(Level.WARNING, "Failed to record results of the Direct Access investigation", e); } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java index a6d96eb91c..b3381a3692 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java @@ -32,8 +32,6 @@ import com.google.cloud.bigtable.data.v2.internal.csm.Metrics; import com.google.cloud.bigtable.data.v2.internal.csm.MetricsImpl; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo; -import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; -import com.google.cloud.bigtable.data.v2.internal.csm.tracers.NoopDirectPathCompatibleTracer; import com.google.cloud.bigtable.data.v2.internal.dp.ClassicDirectAccessChecker; import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessChecker; import com.google.cloud.bigtable.data.v2.internal.dp.NoopDirectAccessChecker; @@ -166,15 +164,10 @@ public static BigtableClientContext create( builder.getHeaderProvider().getHeaders()); } - DirectPathCompatibleTracer directPathCompatibleTracer = - settings.isDirectPathEnabledByDefault() - ? metrics.getDirectPathCompatibleTracer() - : NoopDirectPathCompatibleTracer.INSTANCE; - DirectAccessChecker directAccessChecker = settings.isDirectPathEnabledByDefault() ? new ClassicDirectAccessChecker( - directPathCompatibleTracer, channelPrimer, backgroundExecutor) + metrics.getDirectPathCompatibleTracer(), channelPrimer, backgroundExecutor) : NoopDirectAccessChecker.INSTANCE; BigtableTransportChannelProvider btTransportProvider = diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/NoopMetricsProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/NoopMetricsProvider.java index 2ccb64a890..a2d1e54ecd 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/NoopMetricsProvider.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/NoopMetricsProvider.java @@ -15,6 +15,8 @@ */ package com.google.cloud.bigtable.data.v2.stub.metrics; +import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util; +import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer; import com.google.common.base.MoreObjects; /** @@ -33,4 +35,23 @@ private NoopMetricsProvider() {} public String toString() { return MoreObjects.toStringHelper(this).toString(); } + + /** A no-op implementation of {@link DirectPathCompatibleTracer}. */ + public static final class NoopDirectPathCompatibleTracer implements DirectPathCompatibleTracer { + + public static final NoopDirectPathCompatibleTracer INSTANCE = + new NoopDirectPathCompatibleTracer(); + + private NoopDirectPathCompatibleTracer() {} + + @Override + public void recordSuccess(Util.IpProtocol ipProtocol) { + // No-op + } + + @Override + public void recordFailure(String reason) { + // No-op + } + } } From 8684bb4e25e7ce811542658dfe47c4c60c1ae000 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Tue, 31 Mar 2026 16:48:20 -0400 Subject: [PATCH 26/28] add --- .../csm/metrics/ClientDpCompatGuage.java | 5 +- .../tracers/DirectPathCompatibleTracer.java | 3 +- .../DirectPathCompatibleTracerImpl.java | 8 ++- .../dp/AlwaysEnabledDirectAccessChecker.java | 43 +++++++++++++++ .../dp/ClassicDirectAccessChecker.java | 7 ++- .../internal/dp/DirectAccessInvestigator.java | 2 +- .../data/v2/stub/BigtableClientContext.java | 18 ++++-- .../v2/stub/EnhancedBigtableStubSettings.java | 55 ++++++++++--------- .../v2/stub/metrics/NoopMetricsProvider.java | 3 +- .../bigtable/gaxx/grpc/ChannelPrimer.java | 10 +++- .../v2/BigtableDataClientFactoryTest.java | 3 +- .../csm/MetricRegistryExportTest.java | 6 +- .../dp/ClassicDirectAccessCheckerTest.java | 4 +- .../EnhancedBigtableStubSettingsTest.java | 2 +- .../v2/stub/EnhancedBigtableStubTest.java | 14 ----- 15 files changed, 119 insertions(+), 64 deletions(-) create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/AlwaysEnabledDirectAccessChecker.java diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java index 9746e67448..3b9cba2c8b 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java @@ -18,6 +18,7 @@ import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo; import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels; import com.google.cloud.bigtable.data.v2.internal.csm.schema.ClientSchema; +import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessInvestigator; import io.opentelemetry.api.metrics.LongGauge; import io.opentelemetry.api.metrics.Meter; @@ -60,12 +61,12 @@ public void recordSuccess(ClientInfo clientInfo, String ipPreference) { } // TODO: replace reason with an enum - public void recordFailure(ClientInfo clientInfo, String reason) { + public void recordFailure(ClientInfo clientInfo, DirectAccessInvestigator.FailureReason reason) { instrument.set( 1, getSchema() .createResourceAttrs(clientInfo) - .put(MetricLabels.DP_REASON_KEY, reason) + .put(MetricLabels.DP_REASON_KEY, reason.getValue()) .put(MetricLabels.DP_IP_PREFERENCE_KEY, "") .build()); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java index db8228b709..701e93ef0d 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracer.java @@ -17,6 +17,7 @@ import com.google.api.core.InternalApi; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util; +import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessInvestigator; /** Interface for recording DirectPath/DirectAccess eligibility metrics. */ @InternalApi @@ -35,5 +36,5 @@ public interface DirectPathCompatibleTracer { * @param reason The reason for the failure (e.g., "routing_check_failed"). */ // TODO: Make this an enum - void recordFailure(String reason); + void recordFailure(DirectAccessInvestigator.FailureReason reason); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracerImpl.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracerImpl.java index d4a66d3d11..f121f897ad 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracerImpl.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracerImpl.java @@ -19,6 +19,8 @@ import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util; +import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessInvestigator; +import com.google.common.base.Preconditions; @InternalApi public class DirectPathCompatibleTracerImpl implements DirectPathCompatibleTracer { @@ -27,8 +29,8 @@ public class DirectPathCompatibleTracerImpl implements DirectPathCompatibleTrace public DirectPathCompatibleTracerImpl( ClientInfo clientInfo, MetricRegistry.RecorderRegistry recorder) { - this.clientInfo = clientInfo; - this.recorder = recorder; + this.clientInfo = Preconditions.checkNotNull(clientInfo); + this.recorder = Preconditions.checkNotNull(recorder); } @Override @@ -37,7 +39,7 @@ public void recordSuccess(Util.IpProtocol ipProtocol) { } @Override - public void recordFailure(String reason) { + public void recordFailure(DirectAccessInvestigator.FailureReason reason) { recorder.dpCompatGuage.recordFailure(clientInfo, reason); } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/AlwaysEnabledDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/AlwaysEnabledDirectAccessChecker.java new file mode 100644 index 0000000000..37db4b04dc --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/AlwaysEnabledDirectAccessChecker.java @@ -0,0 +1,43 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.bigtable.data.v2.internal.dp; + +import com.google.api.core.InternalApi; +import io.grpc.Channel; +import io.grpc.ManagedChannel; +import javax.annotation.Nullable; + +@InternalApi +public class AlwaysEnabledDirectAccessChecker implements DirectAccessChecker { + public static final AlwaysEnabledDirectAccessChecker INSTANCE = + new AlwaysEnabledDirectAccessChecker(); + + private AlwaysEnabledDirectAccessChecker() {} + + @Override + public boolean check(Channel channel) { + if (channel instanceof ManagedChannel) { + ((ManagedChannel) channel).shutdownNow(); + } + return true; + } + + @Override + public void investigateFailure(@Nullable Throwable originalError) { + // No-op: + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java index 64ed857a98..0885066606 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java @@ -21,6 +21,7 @@ import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor; import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import io.grpc.Channel; import io.grpc.ClientInterceptors; import io.grpc.ManagedChannel; @@ -45,9 +46,9 @@ public ClassicDirectAccessChecker( DirectPathCompatibleTracer tracer, ChannelPrimer channelPrimer, ScheduledExecutorService executor) { - this.tracer = tracer; - this.channelPrimer = channelPrimer; - this.executor = executor; + this.tracer = Preconditions.checkNotNull(tracer); + this.channelPrimer = Preconditions.checkNotNull(channelPrimer); + this.executor = Preconditions.checkNotNull(executor); } @VisibleForTesting diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java index 32bcb41203..2a80672d82 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessInvestigator.java @@ -75,6 +75,6 @@ private static void recordAndLog( } else { LOG.log(Level.FINE, logMessage); } - tracer.recordFailure(reason.getValue()); + tracer.recordFailure(reason); } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java index b3381a3692..d57f7d1fb2 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java @@ -32,6 +32,7 @@ import com.google.cloud.bigtable.data.v2.internal.csm.Metrics; import com.google.cloud.bigtable.data.v2.internal.csm.MetricsImpl; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo; +import com.google.cloud.bigtable.data.v2.internal.dp.AlwaysEnabledDirectAccessChecker; import com.google.cloud.bigtable.data.v2.internal.dp.ClassicDirectAccessChecker; import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessChecker; import com.google.cloud.bigtable.data.v2.internal.dp.NoopDirectAccessChecker; @@ -164,12 +165,17 @@ public static BigtableClientContext create( builder.getHeaderProvider().getHeaders()); } - DirectAccessChecker directAccessChecker = - settings.isDirectPathEnabledByDefault() - ? new ClassicDirectAccessChecker( - metrics.getDirectPathCompatibleTracer(), channelPrimer, backgroundExecutor) - : NoopDirectAccessChecker.INSTANCE; - + DirectAccessChecker directAccessChecker = null; + switch (settings.getDirectPathConfig()) { + case FORCED_ON: + directAccessChecker = AlwaysEnabledDirectAccessChecker.INSTANCE; + break; + case FORCED_OFF: + case DEFAULT: + directAccessChecker = NoopDirectAccessChecker.INSTANCE; + break; + } + BigtableTransportChannelProvider btTransportProvider = BigtableTransportChannelProvider.create( transportProvider.build(), diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index 9312bfe143..aa98627dc8 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -97,16 +97,17 @@ public class EnhancedBigtableStubSettings extends StubSettings b ? DirectPathConfig.FORCED_ON : DirectPathConfig.FORCED_OFF).orElse(DirectPathConfig.DEFAULT); + + // If true, disable the bound-token-by-default feature for DirectPath. + private static final boolean DIRECT_PATH_BOUND_TOKEN_DISABLED = + Boolean.parseBoolean(System.getenv("CBT_DISABLE_DIRECTPATH_BOUND_TOKEN")); /** * Scopes that are equivalent to JWT's audience. @@ -141,13 +142,19 @@ public class EnhancedBigtableStubSettings extends StubSettings { - private boolean enableDirectPathByDefault; + private DirectPathConfig directPathConfig; private String projectId; private String instanceId; private String appProfileId; @@ -605,8 +612,8 @@ public static class Builder extends StubSettings.Builder sendPrimeRequestsAsync(ManagedChannel channel) { return sendPrimeRequestsAsync((Channel) channel); diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java index 62ce89d4b9..b098ac5ec8 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java @@ -37,6 +37,7 @@ import com.google.cloud.bigtable.data.v2.internal.NameUtil; import com.google.cloud.bigtable.data.v2.models.RowMutation; import com.google.cloud.bigtable.data.v2.models.TableId; +import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStubSettings; import com.google.cloud.bigtable.data.v2.stub.metrics.NoopMetricsProvider; import com.google.common.base.Preconditions; import com.google.common.io.BaseEncoding; @@ -268,7 +269,7 @@ public void testCreateWithRefreshingChannel() throws Exception { .setCredentialsProvider(credentialsProvider) .setStreamWatchdogProvider(watchdogProvider) .setBackgroundExecutorProvider(executorProvider) - .setEnableDirectPathByDefault(true); + .setDirectPathConfig(EnhancedBigtableStubSettings.DirectPathConfig.FORCED_ON); InstantiatingGrpcChannelProvider channelProvider = (InstantiatingGrpcChannelProvider) builder.stubSettings().getTransportChannelProvider(); InstantiatingGrpcChannelProvider.Builder channelProviderBuilder = channelProvider.toBuilder(); diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistryExportTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistryExportTest.java index 974ac41868..19ac34dcfc 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistryExportTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistryExportTest.java @@ -16,6 +16,7 @@ package com.google.cloud.bigtable.data.v2.internal.csm; +import static com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessInvestigator.FailureReason.UNKNOWN; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; @@ -35,6 +36,7 @@ import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo; import com.google.cloud.bigtable.data.v2.internal.csm.exporter.BigtableCloudMonitoringExporter; +import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessInvestigator; import com.google.cloud.bigtable.gaxx.grpc.BigtableChannelPoolSettings.LoadBalancingStrategy; import com.google.cloud.monitoring.v3.MetricServiceClient; import com.google.cloud.monitoring.v3.MetricServiceSettings; @@ -440,7 +442,7 @@ void testConnectivityErrors() { @Test void testDpCompatGuage() { - registry.dpCompatGuage.recordFailure(clientInfo, "something"); + registry.dpCompatGuage.recordFailure(clientInfo, UNKNOWN); registry.dpCompatGuage.recordSuccess(clientInfo, "ipv4"); metricReader.forceFlush().join(1, TimeUnit.MINUTES); @@ -464,7 +466,7 @@ void testDpCompatGuage() { "reason", "", "ip_preference", "ipv4"), ImmutableMap.of( - "reason", "something", + "reason", UNKNOWN.getValue(), "ip_preference", "")); } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java index 33e6a74f9b..854f5c4761 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java @@ -60,7 +60,7 @@ public class ClassicDirectAccessCheckerTest { @Before public void setUp() throws Exception { // Pass null for the executor by default so background investigations aren't triggered - checker = spy(new ClassicDirectAccessChecker(mockTracer, mockChannelPrimer, null)); + checker = spy(new ClassicDirectAccessChecker(mockTracer, mockChannelPrimer, mockExecutor)); doReturn(mockInterceptor).when(checker).createInterceptor(); when(mockInterceptor.getSidebandData()).thenReturn(mockSidebandData); } @@ -150,6 +150,6 @@ public void testInvestigationTriggeredOnFailure() { // Execute the captured runnable to ensure it safely calls the tracer runnableCaptor.getValue().run(); - verify(mockTracer).recordFailure(anyString()); + verify(mockTracer).recordFailure(DirectAccessInvestigator.FailureReason.UNKNOWN); } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java index 520204ed85..2f243978d1 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java @@ -905,7 +905,7 @@ public void isRefreshingChannelFalseValueTest() { "metricsEndpoint", "areInternalMetricsEnabled", "jwtAudience", - "enableDirectPathByDefault", + "directPathConfig", }; @Test diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java index 276784bf14..bfc94dea50 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java @@ -557,20 +557,6 @@ public void testChannelPrimerConfigured() throws IOException { } } - @Test - public void testChannelPrimeWithDirectAccessEnabledByDefault() throws IOException { - EnhancedBigtableStubSettings settings = - defaultSettings.toBuilder() - .setRefreshingChannel(true) - .setEnableDirectPathByDefault(true) - .build(); - - try (EnhancedBigtableStub ignored = EnhancedBigtableStub.create(settings)) { - // direct access checker ping - assertThat(fakeDataService.pingRequests).hasSize(2); - } - } - @Test public void testUserAgent() throws InterruptedException { ServerStreamingCallable streamingCallable = From c1e2485109c8b34ed2b1025e3a7a781adbe9d527 Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Tue, 31 Mar 2026 17:02:26 -0400 Subject: [PATCH 27/28] wip --- .../csm/metrics/ClientDpCompatGuage.java | 3 ++- .../DirectPathCompatibleTracerImpl.java | 2 +- .../dp/AlwaysEnabledDirectAccessChecker.java | 26 +++++++++---------- .../internal/dp/NoopDirectAccessChecker.java | 5 ++++ .../data/v2/stub/BigtableClientContext.java | 3 +-- .../v2/stub/EnhancedBigtableStubSettings.java | 15 +++++++---- .../v2/stub/MetadataExtractorInterceptor.java | 1 - .../BigtableTransportChannelProvider.java | 2 +- .../bigtable/gaxx/grpc/ChannelPrimer.java | 4 +-- .../v2/BigtableDataClientFactoryTest.java | 6 ++--- .../csm/MetricRegistryExportTest.java | 5 +--- .../dp/ClassicDirectAccessCheckerTest.java | 1 - 12 files changed, 38 insertions(+), 35 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java index 3b9cba2c8b..26bc493ed1 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java @@ -61,7 +61,8 @@ public void recordSuccess(ClientInfo clientInfo, String ipPreference) { } // TODO: replace reason with an enum - public void recordFailure(ClientInfo clientInfo, DirectAccessInvestigator.FailureReason reason) { + public void recordFailure( + ClientInfo clientInfo, DirectAccessInvestigator.FailureReason reason) { instrument.set( 1, getSchema() diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracerImpl.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracerImpl.java index f121f897ad..9995d1c90d 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracerImpl.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/tracers/DirectPathCompatibleTracerImpl.java @@ -30,7 +30,7 @@ public class DirectPathCompatibleTracerImpl implements DirectPathCompatibleTrace public DirectPathCompatibleTracerImpl( ClientInfo clientInfo, MetricRegistry.RecorderRegistry recorder) { this.clientInfo = Preconditions.checkNotNull(clientInfo); - this.recorder = Preconditions.checkNotNull(recorder); + this.recorder = Preconditions.checkNotNull(recorder); } @Override diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/AlwaysEnabledDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/AlwaysEnabledDirectAccessChecker.java index 37db4b04dc..307f55a18d 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/AlwaysEnabledDirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/AlwaysEnabledDirectAccessChecker.java @@ -23,21 +23,21 @@ @InternalApi public class AlwaysEnabledDirectAccessChecker implements DirectAccessChecker { - public static final AlwaysEnabledDirectAccessChecker INSTANCE = - new AlwaysEnabledDirectAccessChecker(); + public static final AlwaysEnabledDirectAccessChecker INSTANCE = + new AlwaysEnabledDirectAccessChecker(); - private AlwaysEnabledDirectAccessChecker() {} + private AlwaysEnabledDirectAccessChecker() {} - @Override - public boolean check(Channel channel) { - if (channel instanceof ManagedChannel) { - ((ManagedChannel) channel).shutdownNow(); - } - return true; + @Override + public boolean check(Channel channel) { + if (channel instanceof ManagedChannel) { + ((ManagedChannel) channel).shutdownNow(); } + return true; + } - @Override - public void investigateFailure(@Nullable Throwable originalError) { - // No-op: - } + @Override + public void investigateFailure(@Nullable Throwable originalError) { + // No-op: + } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/NoopDirectAccessChecker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/NoopDirectAccessChecker.java index cf727996ac..bf79fd5478 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/NoopDirectAccessChecker.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/NoopDirectAccessChecker.java @@ -17,6 +17,7 @@ import com.google.api.core.InternalApi; import io.grpc.Channel; +import io.grpc.ManagedChannel; import javax.annotation.Nullable; @InternalApi @@ -27,6 +28,10 @@ private NoopDirectAccessChecker() {} @Override public boolean check(Channel channel) { + // We must shut down the temporary probe channel to prevent gRPC resource leaks! + if (channel instanceof ManagedChannel) { + ((ManagedChannel) channel).shutdownNow(); + } // If it's disabled, it is never eligible. return false; } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java index d57f7d1fb2..c4f882e2f7 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java @@ -33,7 +33,6 @@ import com.google.cloud.bigtable.data.v2.internal.csm.MetricsImpl; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo; import com.google.cloud.bigtable.data.v2.internal.dp.AlwaysEnabledDirectAccessChecker; -import com.google.cloud.bigtable.data.v2.internal.dp.ClassicDirectAccessChecker; import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessChecker; import com.google.cloud.bigtable.data.v2.internal.dp.NoopDirectAccessChecker; import com.google.cloud.bigtable.data.v2.stub.metrics.CustomOpenTelemetryMetricsProvider; @@ -175,7 +174,7 @@ public static BigtableClientContext create( directAccessChecker = NoopDirectAccessChecker.INSTANCE; break; } - + BigtableTransportChannelProvider btTransportProvider = BigtableTransportChannelProvider.create( transportProvider.build(), diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index aa98627dc8..c757c8fb8d 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -101,9 +101,10 @@ public class EnhancedBigtableStubSettings extends StubSettings b ? DirectPathConfig.FORCED_ON : DirectPathConfig.FORCED_OFF).orElse(DirectPathConfig.DEFAULT); + Optional.ofNullable(System.getenv("CBT_ENABLE_DIRECTPATH")) + .map(Boolean::parseBoolean) + .map(b -> b ? DirectPathConfig.FORCED_ON : DirectPathConfig.FORCED_OFF) + .orElse(DirectPathConfig.DEFAULT); // If true, disable the bound-token-by-default feature for DirectPath. private static final boolean DIRECT_PATH_BOUND_TOKEN_DISABLED = @@ -149,6 +150,7 @@ public enum DirectPathConfig { FORCED_ON, FORCED_OFF, } + private final DirectPathConfig directPathConfig; private EnhancedBigtableStubSettings(Builder builder) { @@ -178,6 +180,7 @@ public String getProjectId() { return projectId; } + @InternalApi public DirectPathConfig getDirectPathConfig() { return DIRECT_PATH_CONFIG; } @@ -280,7 +283,8 @@ public static InstantiatingGrpcChannelProvider.Builder defaultGrpcTransportProvi } /** Applies Direct Access traits to an existing builder. */ - public static InstantiatingGrpcChannelProvider.Builder applyDirectAccessTraits( + @InternalApi + public static InstantiatingGrpcChannelProvider.Builder applyDirectAccessTraitsInternal( InstantiatingGrpcChannelProvider.Builder builder) { builder .setAttemptDirectPathXds() @@ -634,7 +638,8 @@ private Builder() { // TODO: flip the bit setDirectAccessRequested and setTrafficDirectorEnabled once we make // client compatible by default. boolean isDirectPathRequested = - directPathConfig == DirectPathConfig.FORCED_ON || directPathConfig == DirectPathConfig.DEFAULT; + directPathConfig == DirectPathConfig.FORCED_ON + || directPathConfig == DirectPathConfig.DEFAULT; featureFlags = FeatureFlags.newBuilder() .setReverseScans(true) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java index 1cefbcabfb..3c4735d2d0 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java @@ -153,7 +153,6 @@ void onClose(Status status, Metadata trailers) { } } - @Nullable private static Util.IpProtocol extractIpProtocol(Attributes attributes) { SocketAddress remoteAddr = attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); if (remoteAddr instanceof InetSocketAddress) { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java index e9521bf193..e0d120d277 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java @@ -164,7 +164,7 @@ public TransportChannelProvider withPoolSize(int size) { @Override public TransportChannel getTransportChannel() throws IOException { InstantiatingGrpcChannelProvider directAccessProvider = - EnhancedBigtableStubSettings.applyDirectAccessTraits(delegate.toBuilder()) + EnhancedBigtableStubSettings.applyDirectAccessTraitsInternal(delegate.toBuilder()) .setChannelPoolSettings(ChannelPoolSettings.staticallySized(1)) .build(); diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java index da91d5235a..29dac62a51 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/ChannelPrimer.java @@ -26,8 +26,8 @@ public interface ChannelPrimer { /** * @deprecated Use {@link #primeChannel(Channel)} */ - @Deprecated - default void primeChannel(ManagedChannel channel) { + @Deprecated + default void primeChannel(ManagedChannel channel) { primeChannel((Channel) channel); } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java index b098ac5ec8..aa8ec31137 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java @@ -287,9 +287,7 @@ public void testCreateWithRefreshingChannel() throws Exception { Mockito.verify(credentialsProvider, Mockito.times(2)).getCredentials(); Mockito.verify(executorProvider, Mockito.times(1)).getExecutor(); Mockito.verify(watchdogProvider, Mockito.times(1)).getWatchdog(); - // +1 accounts for the temporary channel created by UnaryDirectAccessChecker - int expectedChannelCount = poolSize + 1; - assertThat(warmedChannels).hasSize(expectedChannelCount); + assertThat(warmedChannels).hasSize(poolSize); assertThat(warmedChannels.values()).doesNotContain(false); // Wait for all the connections to close asynchronously @@ -297,7 +295,7 @@ public void testCreateWithRefreshingChannel() throws Exception { long sleepTimeMs = 1000; Thread.sleep(sleepTimeMs); // Verify that all the channels are closed - assertThat(terminateAttributes).hasSize(expectedChannelCount); + assertThat(terminateAttributes).hasSize(poolSize); } @Test diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistryExportTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistryExportTest.java index 19ac34dcfc..9e91a6c1d8 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistryExportTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistryExportTest.java @@ -36,7 +36,6 @@ import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo; import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo; import com.google.cloud.bigtable.data.v2.internal.csm.exporter.BigtableCloudMonitoringExporter; -import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessInvestigator; import com.google.cloud.bigtable.gaxx.grpc.BigtableChannelPoolSettings.LoadBalancingStrategy; import com.google.cloud.monitoring.v3.MetricServiceClient; import com.google.cloud.monitoring.v3.MetricServiceSettings; @@ -465,9 +464,7 @@ void testDpCompatGuage() { ImmutableMap.of( "reason", "", "ip_preference", "ipv4"), - ImmutableMap.of( - "reason", UNKNOWN.getValue(), - "ip_preference", "")); + ImmutableMap.of("reason", UNKNOWN.getValue(), "ip_preference", "")); } @Test diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java index 854f5c4761..5ce624cf44 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessCheckerTest.java @@ -17,7 +17,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.spy; From e8d5c210f981323507e68f25ce50a16679690bed Mon Sep 17 00:00:00 2001 From: Sushan Bhattarai Date: Tue, 31 Mar 2026 18:02:12 -0400 Subject: [PATCH 28/28] fix --- .../bigtable/data/v2/stub/EnhancedBigtableStubSettings.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index c757c8fb8d..dcd0879b40 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -637,9 +637,7 @@ private Builder() { // TODO: flip the bit setDirectAccessRequested and setTrafficDirectorEnabled once we make // client compatible by default. - boolean isDirectPathRequested = - directPathConfig == DirectPathConfig.FORCED_ON - || directPathConfig == DirectPathConfig.DEFAULT; + boolean isDirectPathRequested = directPathConfig == DirectPathConfig.FORCED_ON; featureFlags = FeatureFlags.newBuilder() .setReverseScans(true)