Skip to content

Commit c2ccda1

Browse files
authored
feat(bigtable): classic direct access checker and it's implementation (#2840)
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/java-bigtable/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) - [ ] Rollback plan is reviewed and LGTMed - [ ] All new data plane features have a completed end to end testing plan Fixes #<issue_number_goes_here> ☕️ If you write sample code, please follow the [samples format]( https://togithub.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).
1 parent 1e00ef2 commit c2ccda1

26 files changed

+827
-74
lines changed

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.google.api.gax.tracing.ApiTracerFactory;
1919
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
2020
import com.google.cloud.bigtable.data.v2.internal.csm.tracers.ChannelPoolMetricsTracer;
21+
import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer;
2122
import io.grpc.ManagedChannelBuilder;
2223
import java.io.Closeable;
2324
import java.io.IOException;
@@ -31,6 +32,8 @@ public interface Metrics extends Closeable {
3132
@Nullable
3233
ChannelPoolMetricsTracer getChannelPoolMetricsTracer();
3334

35+
DirectPathCompatibleTracer getDirectPathCompatibleTracer();
36+
3437
void start();
3538

3639
@Override

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
import com.google.cloud.bigtable.data.v2.internal.csm.tracers.BuiltinMetricsTracerFactory;
3232
import com.google.cloud.bigtable.data.v2.internal.csm.tracers.ChannelPoolMetricsTracer;
3333
import com.google.cloud.bigtable.data.v2.internal.csm.tracers.CompositeTracerFactory;
34+
import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer;
35+
import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracerImpl;
3436
import com.google.cloud.bigtable.data.v2.internal.csm.tracers.Pacemaker;
37+
import com.google.cloud.bigtable.data.v2.stub.metrics.NoopMetricsProvider;
3538
import com.google.common.base.Preconditions;
3639
import com.google.common.base.Suppliers;
3740
import com.google.common.collect.ImmutableList;
@@ -68,6 +71,7 @@ public class MetricsImpl implements Metrics, Closeable {
6871

6972
@Nullable private final GrpcOpenTelemetry grpcOtel;
7073
@Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer;
74+
private final DirectPathCompatibleTracer directPathCompatibleTracer;
7175
@Nullable private final Pacemaker pacemaker;
7276
private final List<ScheduledFuture<?>> tasks = new ArrayList<>();
7377

@@ -95,6 +99,8 @@ public MetricsImpl(
9599
this.internalRecorder = metricRegistry.newRecorderRegistry(internalOtel.getMeterProvider());
96100
this.pacemaker = new Pacemaker(internalRecorder, clientInfo, "background");
97101
this.channelPoolMetricsTracer = new ChannelPoolMetricsTracer(internalRecorder, clientInfo);
102+
this.directPathCompatibleTracer =
103+
new DirectPathCompatibleTracerImpl(clientInfo, internalRecorder);
98104
this.grpcOtel =
99105
GrpcOpenTelemetry.newBuilder()
100106
.sdk(internalOtel)
@@ -110,6 +116,7 @@ public MetricsImpl(
110116
this.grpcOtel = null;
111117
this.pacemaker = null;
112118
this.channelPoolMetricsTracer = null;
119+
this.directPathCompatibleTracer = NoopMetricsProvider.NoopDirectPathCompatibleTracer.INSTANCE;
113120
}
114121

115122
if (userOtel != null) {
@@ -172,6 +179,11 @@ public ChannelPoolMetricsTracer getChannelPoolMetricsTracer() {
172179
return channelPoolMetricsTracer;
173180
}
174181

182+
@Override
183+
public DirectPathCompatibleTracer getDirectPathCompatibleTracer() {
184+
return directPathCompatibleTracer;
185+
}
186+
175187
public static OpenTelemetrySdk createBuiltinOtel(
176188
MetricRegistry metricRegistry,
177189
ClientInfo clientInfo,

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@
4040
import javax.annotation.Nullable;
4141

4242
public class Util {
43+
public enum IpProtocol {
44+
IPV4("ipv4"),
45+
IPV6("ipv6"),
46+
UNKNOWN("unknown");
47+
48+
private final String value;
49+
50+
IpProtocol(String value) {
51+
this.value = value;
52+
}
53+
54+
public String getValue() {
55+
return value;
56+
}
57+
}
58+
4359
static final String TRANSPORT_TYPE_PREFIX = "TRANSPORT_TYPE_";
4460

4561
public static String formatTransportZone(@Nullable PeerInfo peerInfo) {

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
1919
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
2020
import com.google.cloud.bigtable.data.v2.internal.csm.schema.ClientSchema;
21+
import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessInvestigator;
2122
import io.opentelemetry.api.metrics.LongGauge;
2223
import io.opentelemetry.api.metrics.Meter;
2324

@@ -60,12 +61,13 @@ public void recordSuccess(ClientInfo clientInfo, String ipPreference) {
6061
}
6162

6263
// TODO: replace reason with an enum
63-
public void recordFailure(ClientInfo clientInfo, String reason) {
64+
public void recordFailure(
65+
ClientInfo clientInfo, DirectAccessInvestigator.FailureReason reason) {
6466
instrument.set(
6567
1,
6668
getSchema()
6769
.createResourceAttrs(clientInfo)
68-
.put(MetricLabels.DP_REASON_KEY, reason)
70+
.put(MetricLabels.DP_REASON_KEY, reason.getValue())
6971
.put(MetricLabels.DP_IP_PREFERENCE_KEY, "")
7072
.build());
7173
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal.csm.tracers;
17+
18+
import com.google.api.core.InternalApi;
19+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util;
20+
import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessInvestigator;
21+
22+
/** Interface for recording DirectPath/DirectAccess eligibility metrics. */
23+
@InternalApi
24+
public interface DirectPathCompatibleTracer {
25+
26+
/**
27+
* Records that the environment is eligible and successfully connected via DirectPath.
28+
*
29+
* @param ipProtocol The IP protocol used (e.g., "ipv6").
30+
*/
31+
void recordSuccess(Util.IpProtocol ipProtocol);
32+
33+
/**
34+
* Records that the environment is not eligible or failed to connect via DirectPath.
35+
*
36+
* @param reason The reason for the failure (e.g., "routing_check_failed").
37+
*/
38+
// TODO: Make this an enum
39+
void recordFailure(DirectAccessInvestigator.FailureReason reason);
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal.csm.tracers;
17+
18+
import com.google.api.core.InternalApi;
19+
import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry;
20+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
21+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util;
22+
import com.google.cloud.bigtable.data.v2.internal.dp.DirectAccessInvestigator;
23+
import com.google.common.base.Preconditions;
24+
25+
@InternalApi
26+
public class DirectPathCompatibleTracerImpl implements DirectPathCompatibleTracer {
27+
private final ClientInfo clientInfo;
28+
private final MetricRegistry.RecorderRegistry recorder;
29+
30+
public DirectPathCompatibleTracerImpl(
31+
ClientInfo clientInfo, MetricRegistry.RecorderRegistry recorder) {
32+
this.clientInfo = Preconditions.checkNotNull(clientInfo);
33+
this.recorder = Preconditions.checkNotNull(recorder);
34+
}
35+
36+
@Override
37+
public void recordSuccess(Util.IpProtocol ipProtocol) {
38+
recorder.dpCompatGuage.recordSuccess(clientInfo, ipProtocol.getValue());
39+
}
40+
41+
@Override
42+
public void recordFailure(DirectAccessInvestigator.FailureReason reason) {
43+
recorder.dpCompatGuage.recordFailure(clientInfo, reason);
44+
}
45+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.bigtable.data.v2.internal.dp;
18+
19+
import com.google.api.core.InternalApi;
20+
import io.grpc.Channel;
21+
import io.grpc.ManagedChannel;
22+
import javax.annotation.Nullable;
23+
24+
@InternalApi
25+
public class AlwaysEnabledDirectAccessChecker implements DirectAccessChecker {
26+
public static final AlwaysEnabledDirectAccessChecker INSTANCE =
27+
new AlwaysEnabledDirectAccessChecker();
28+
29+
private AlwaysEnabledDirectAccessChecker() {}
30+
31+
@Override
32+
public boolean check(Channel channel) {
33+
if (channel instanceof ManagedChannel) {
34+
((ManagedChannel) channel).shutdownNow();
35+
}
36+
return true;
37+
}
38+
39+
@Override
40+
public void investigateFailure(@Nullable Throwable originalError) {
41+
// No-op:
42+
}
43+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal.dp;
17+
18+
import com.google.api.core.InternalApi;
19+
import com.google.bigtable.v2.PeerInfo;
20+
import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer;
21+
import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor;
22+
import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer;
23+
import com.google.common.annotations.VisibleForTesting;
24+
import com.google.common.base.Preconditions;
25+
import io.grpc.Channel;
26+
import io.grpc.ClientInterceptors;
27+
import io.grpc.ManagedChannel;
28+
import java.util.Optional;
29+
import java.util.concurrent.ScheduledExecutorService;
30+
import java.util.logging.Level;
31+
import java.util.logging.Logger;
32+
import javax.annotation.Nullable;
33+
34+
/**
35+
* Evaluates whether a given channel has Direct Access (DirectPath) routing by executing a RPC and
36+
* inspecting the response headers.
37+
*/
38+
@InternalApi
39+
public class ClassicDirectAccessChecker implements DirectAccessChecker {
40+
private static final Logger LOG = Logger.getLogger(ClassicDirectAccessChecker.class.getName());
41+
private final DirectPathCompatibleTracer tracer;
42+
private final ChannelPrimer channelPrimer;
43+
private final ScheduledExecutorService executor;
44+
45+
public ClassicDirectAccessChecker(
46+
DirectPathCompatibleTracer tracer,
47+
ChannelPrimer channelPrimer,
48+
ScheduledExecutorService executor) {
49+
this.tracer = Preconditions.checkNotNull(tracer);
50+
this.channelPrimer = Preconditions.checkNotNull(channelPrimer);
51+
this.executor = Preconditions.checkNotNull(executor);
52+
}
53+
54+
@VisibleForTesting
55+
MetadataExtractorInterceptor createInterceptor() {
56+
return new MetadataExtractorInterceptor();
57+
}
58+
59+
@Override
60+
public boolean check(Channel channel) {
61+
try {
62+
return evaluateEligibility(channel);
63+
} catch (Exception e) {
64+
investigateFailure(e);
65+
LOG.log(Level.WARNING, "Failed to evaluate direct access eligibility.", e);
66+
return false;
67+
} finally {
68+
if (channel instanceof ManagedChannel) {
69+
ManagedChannel managedChannel = (ManagedChannel) channel;
70+
managedChannel.shutdownNow();
71+
}
72+
}
73+
}
74+
75+
/** Executes the underlying RPC and evaluates the eligibility. */
76+
private boolean evaluateEligibility(Channel channel) {
77+
MetadataExtractorInterceptor interceptor = createInterceptor();
78+
Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor);
79+
channelPrimer.primeChannel(interceptedChannel);
80+
MetadataExtractorInterceptor.SidebandData sidebandData = interceptor.getSidebandData();
81+
82+
boolean isEligible =
83+
Optional.ofNullable(sidebandData)
84+
.map(MetadataExtractorInterceptor.SidebandData::getPeerInfo)
85+
.map(PeerInfo::getTransportType)
86+
.map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS)
87+
.orElse(false);
88+
89+
if (isEligible) {
90+
// getIp should be non-null as isEligible is true
91+
tracer.recordSuccess(sidebandData.getIpProtocol());
92+
} else {
93+
investigateFailure(null);
94+
}
95+
return isEligible;
96+
}
97+
98+
@Override
99+
public void investigateFailure(@Nullable Throwable originalError) {
100+
if (executor != null) {
101+
executor.execute(() -> DirectAccessInvestigator.investigateAndReport(tracer, originalError));
102+
}
103+
}
104+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal.dp;
17+
18+
import com.google.api.core.InternalApi;
19+
import io.grpc.Channel;
20+
21+
@InternalApi
22+
/* Evaluates whether a given channel supports Direct Access. */
23+
public interface DirectAccessChecker {
24+
/**
25+
* Evaluates if Direct Access is available by sending request via provided channel.
26+
*
27+
* @param channel A channel to probe direct access connectivity
28+
* @return true if the channel is eligible for Direct Access
29+
*/
30+
boolean check(Channel channel);
31+
32+
/**
33+
* Triggers a investigation into why Direct Access routing failed.
34+
*
35+
* @param originalError An optional exception that caused the failure.
36+
*/
37+
void investigateFailure(Throwable originalError);
38+
}

0 commit comments

Comments
 (0)