Skip to content
This repository was archived by the owner on May 8, 2026. It is now read-only.

Commit f56667a

Browse files
committed
fix and add direct access investigator
1 parent de17a3f commit f56667a

15 files changed

Lines changed: 513 additions & 162 deletions

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/tracers/DefaultDirectPathCompatibleTracer.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.google.api.core.InternalApi;
1919
import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry;
2020
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
21+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util;
2122

2223
@InternalApi
2324
public class DefaultDirectPathCompatibleTracer implements DirectPathCompatibleTracer {
@@ -31,8 +32,8 @@ public DefaultDirectPathCompatibleTracer(
3132
}
3233

3334
@Override
34-
public void recordSuccess(String ipPreference) {
35-
recorder.dpCompatGuage.recordSuccess(clientInfo, ipPreference);
35+
public void recordSuccess(Util.IpProtocol ipProtocol) {
36+
recorder.dpCompatGuage.recordSuccess(clientInfo, ipProtocol.getValue());
3637
}
3738

3839
@Override

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.google.cloud.bigtable.data.v2.internal.csm.tracers;
1717

1818
import com.google.api.core.InternalApi;
19+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util;
1920

2021
/** Interface for recording DirectPath/DirectAccess eligibility metrics. */
2122
@InternalApi
@@ -24,9 +25,9 @@ public interface DirectPathCompatibleTracer {
2425
/**
2526
* Records that the environment is eligible and successfully connected via DirectPath.
2627
*
27-
* @param ipPreference The IP preference used (e.g., "ipv6").
28+
* @param ipProtocol The IP protocol used (e.g., "ipv6").
2829
*/
29-
void recordSuccess(String ipPreference);
30+
void recordSuccess(Util.IpProtocol ipProtocol);
3031

3132
/**
3233
* Records that the environment is not eligible or failed to connect via DirectPath.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.google.cloud.bigtable.data.v2.internal.csm.tracers;
1717

1818
import com.google.api.core.InternalApi;
19+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util;
1920

2021
@InternalApi
2122
public class NoopDirectPathCompatibleTracer implements DirectPathCompatibleTracer {
@@ -26,7 +27,7 @@ public class NoopDirectPathCompatibleTracer implements DirectPathCompatibleTrace
2627
private NoopDirectPathCompatibleTracer() {}
2728

2829
@Override
29-
public void recordSuccess(String ipPreference) {
30+
public void recordSuccess(Util.IpProtocol ipProtocol) {
3031
// No-op
3132
}
3233

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/ClassicDirectAccessChecker.java

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import io.grpc.ClientInterceptors;
2626
import io.grpc.ManagedChannel;
2727
import java.util.Optional;
28-
import java.util.function.Supplier;
2928
import java.util.logging.Level;
3029
import java.util.logging.Logger;
3130

@@ -51,41 +50,40 @@ MetadataExtractorInterceptor createInterceptor() {
5150
}
5251

5352
@Override
54-
public boolean check(Supplier<ManagedChannel> supplier) {
55-
ManagedChannel channel = null;
53+
public boolean check(Channel channel) {
5654
try {
57-
channel = supplier.get();
58-
MetadataExtractorInterceptor interceptor = createInterceptor();
59-
Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor);
60-
channelPrimer.primeChannel(interceptedChannel);
61-
62-
// Extract the sideband data populated by the interceptor
63-
MetadataExtractorInterceptor.SidebandData sidebandData = interceptor.getSidebandData();
64-
65-
boolean isEligible =
66-
Optional.ofNullable(sidebandData)
67-
.map(MetadataExtractorInterceptor.SidebandData::getPeerInfo)
68-
.map(PeerInfo::getTransportType)
69-
.map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS)
70-
.orElse(false);
71-
72-
if (isEligible) {
73-
String ipProtocolStr =
74-
sidebandData.getIpProtocol() != null
75-
? sidebandData.getIpProtocol().toString().toLowerCase()
76-
: "unknown";
77-
tracer.recordSuccess(ipProtocolStr);
78-
}
79-
80-
return isEligible;
81-
55+
return evaluateEligibility(channel);
8256
} catch (Exception e) {
83-
LOG.log(Level.FINE, "Failed to evaluate direct access eligibility.", e);
57+
LOG.log(Level.WARNING, "Failed to evaluate direct access eligibility.", e);
8458
return false;
8559
} finally {
86-
if (channel != null) {
87-
channel.shutdownNow();
60+
if (channel instanceof ManagedChannel) {
61+
ManagedChannel managedChannel = (ManagedChannel) channel;
62+
managedChannel.shutdownNow();
8863
}
8964
}
9065
}
66+
67+
/**
68+
* Executes the underlying RPC and evaluates the eligibility.
69+
*/
70+
private boolean evaluateEligibility(Channel channel) {
71+
MetadataExtractorInterceptor interceptor = createInterceptor();
72+
Channel interceptedChannel = ClientInterceptors.intercept(channel, interceptor);
73+
channelPrimer.primeChannel(interceptedChannel);
74+
MetadataExtractorInterceptor.SidebandData sidebandData = interceptor.getSidebandData();
75+
76+
boolean isEligible =
77+
Optional.ofNullable(sidebandData)
78+
.map(MetadataExtractorInterceptor.SidebandData::getPeerInfo)
79+
.map(PeerInfo::getTransportType)
80+
.map(type -> type == PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS)
81+
.orElse(false);
82+
83+
if (isEligible) {
84+
// getIp should be non-null as isEligible is true
85+
tracer.recordSuccess(sidebandData.getIpProtocol());
86+
}
87+
return isEligible;
88+
}
9189
}

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/dp/DirectAccessChecker.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,16 @@
1616
package com.google.cloud.bigtable.data.v2.internal.dp;
1717

1818
import com.google.api.core.InternalApi;
19-
import io.grpc.ManagedChannel;
20-
import java.util.function.Supplier;
19+
import io.grpc.Channel;
2120

2221
@InternalApi
2322
/* Evaluates whether a given channel supports Direct Access. */
2423
public interface DirectAccessChecker {
2524
/**
26-
* Evaluates if Direct Access is available by creating a test channel.
25+
* Evaluates if Direct Access is available by sending request via provided channel.
2726
*
28-
* @param supplier A supplier to create maybe direct access channel
27+
* @param channel A channel to probe direct access connectivity
2928
* @return true if the channel is eligible for Direct Access
3029
*/
31-
boolean check(Supplier<ManagedChannel> supplier);
30+
boolean check(Channel channel);
3231
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,141 @@
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+
117
package com.google.cloud.bigtable.data.v2.internal.dp;
218

19+
import com.google.api.core.InternalApi;
20+
import com.google.cloud.bigtable.data.v2.internal.csm.tracers.DirectPathCompatibleTracer;
21+
import java.net.InetAddress;
22+
import java.util.logging.Level;
23+
import java.util.logging.Logger;
24+
25+
@InternalApi
326
public class DirectAccessInvestigator {
27+
private static final Logger LOG = Logger.getLogger(DirectAccessInvestigator.class.getName());
28+
29+
// Telemetry metric reason codes
30+
private static final String REASON_NOT_IN_GCP = "not_in_gcp";
31+
private static final String REASON_METADATA_UNREACHABLE = "metadata_unreachable";
32+
private static final String REASON_NO_IP_ASSIGNED = "no_ip_assigned";
33+
private static final String REASON_LOOPBACK_DOWN = "loopback_misconfigured";
34+
private static final String REASON_LOOPBACK_V4_MISSING = "loopback_misconfigured_ipv4";
35+
private static final String REASON_LOOPBACK_V6_MISSING = "loopback_misconfigured_ipv6";
36+
private static final String REASON_UNKNOWN = "";
37+
38+
public static void investigateAndReport(
39+
DirectPathCompatibleTracer tracer, Throwable originalError) {
40+
try {
41+
if (!GCECheck.isRunningOnGCP()) {
42+
recordAndLog(
43+
tracer, REASON_NOT_IN_GCP, "Direct Access investigation: not in GCP.", originalError);
44+
return;
45+
}
46+
47+
if (!MetadataServer.isReachable()) {
48+
recordAndLog(
49+
tracer,
50+
REASON_METADATA_UNREACHABLE,
51+
"Direct Access investigation: Metadata unreachable.",
52+
null);
53+
return;
54+
}
55+
56+
InetAddress ipv4 = MetadataServer.getIPv4();
57+
InetAddress ipv6 = MetadataServer.getIPv6();
58+
59+
if (ipv4 == null && ipv6 == null) {
60+
recordAndLog(
61+
tracer,
62+
REASON_NO_IP_ASSIGNED,
63+
"Direct Access investigation: Neither IPv4 nor IPv6 assigned.",
64+
null);
65+
return;
66+
}
67+
68+
if (!LoopBackInterface.isUp()) {
69+
recordAndLog(
70+
tracer,
71+
REASON_LOOPBACK_DOWN,
72+
"Direct Access investigation: Loopback interface down.",
73+
null);
74+
return;
75+
}
76+
77+
// ONLY check for IPv4 loopback if we are using an IPv4 address
78+
if (ipv4 != null && !LoopBackInterface.hasLocalIpv4Loopback()) {
79+
recordAndLog(
80+
tracer,
81+
REASON_LOOPBACK_V4_MISSING,
82+
"Direct Access investigation: IPv4 loopback missing.",
83+
null);
84+
return;
85+
}
86+
87+
// ONLY check for IPv6 loopback if we are using an IPv6 address
88+
if (ipv6 != null && !LoopBackInterface.hasLocalIpv6Loopback()) {
89+
recordAndLog(
90+
tracer,
91+
REASON_LOOPBACK_V6_MISSING,
92+
"Direct Access investigation: IPv6 loopback missing.",
93+
null);
94+
return;
95+
}
96+
97+
boolean v4Plumbed = ipv4 != null && LoopBackInterface.isIpPlumbed(ipv4);
98+
if (ipv4 != null && !v4Plumbed) {
99+
LOG.log(
100+
Level.FINE,
101+
"Direct Access investigation: IPv4 assigned by metadata but not found on NIC.");
102+
}
103+
104+
boolean v6Plumbed = ipv6 != null && LoopBackInterface.isIpPlumbed(ipv6);
105+
if (ipv6 != null && !v6Plumbed) {
106+
LOG.log(
107+
Level.FINE,
108+
"Direct Access investigation: IPv6 assigned by metadata but not found on NIC.");
109+
}
110+
111+
// If the metadata server assigned IPs, but the guest OS hasn't configured any of them on an
112+
// interface.
113+
// Do NOT return early here, because this is how GKE pods work (relying on default kernel
114+
// routing).
115+
if (!v4Plumbed && !v6Plumbed) {
116+
LOG.log(
117+
Level.FINE,
118+
"Direct Access investigation: Metadata IPs are not plumbed to local interfaces (likely containerized). Relying on kernel default routing.");
119+
}
120+
recordAndLog(
121+
tracer,
122+
REASON_UNKNOWN,
123+
"Direct Access investigation: Running on GCP, metadata reachable, IPs assigned and plumbed, but Direct Access still failed.",
124+
originalError);
125+
126+
} catch (Exception e) {
127+
LOG.log(Level.WARNING, "Failed to complete Direct Access investigation", e);
128+
}
129+
}
130+
131+
/** Helper method to consistently log the failure reason and record it to the tracer. */
132+
private static void recordAndLog(
133+
DirectPathCompatibleTracer tracer, String reasonCode, String logMessage, Throwable error) {
134+
if (error != null) {
135+
LOG.log(Level.FINE, logMessage, error);
136+
} else {
137+
LOG.log(Level.FINE, logMessage);
138+
}
139+
tracer.recordFailure(reasonCode);
140+
}
4141
}

0 commit comments

Comments
 (0)