Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ and what APIs have changed, if applicable.

## [Unreleased]

## [29.74.4] - 2025-09-03
- Add a pre-check step for initializing xDS stream connections

## [29.74.3] - 2025-08-26
- Add outlier detection config into D2Cluster

Expand Down Expand Up @@ -5879,7 +5882,8 @@ patch operations can re-use these classes for generating patch messages.

## [0.14.1]

[Unreleased]: https://github.com/linkedin/rest.li/compare/v29.74.3...master
[Unreleased]: https://github.com/linkedin/rest.li/compare/v29.74.4...master
[29.74.4]: https://github.com/linkedin/rest.li/compare/v29.74.3...v29.74.4
[29.74.3]: https://github.com/linkedin/rest.li/compare/v29.74.2...v29.74.3
[29.74.2]: https://github.com/linkedin/rest.li/compare/v29.74.1...v29.74.2
[29.74.1]: https://github.com/linkedin/rest.li/compare/v29.74.0...v29.74.1
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ project.ext.externalDependency = [
'grpcNettyShaded' : 'io.grpc:grpc-netty-shaded:1.68.3',
'grpcProtobuf' : 'io.grpc:grpc-protobuf:1.68.3',
'grpcStub' : 'io.grpc:grpc-stub:1.68.3',
'grpcServices' : 'io.grpc:grpc-services:1.68.3',
'protoc' : 'com.google.protobuf:protoc:3.25.5',
'protobufJava' : 'com.google.protobuf:protobuf-java:3.25.5',
'protobufJavaUtil' : 'com.google.protobuf:protobuf-java-util:3.25.5',
Expand Down
1 change: 1 addition & 0 deletions d2/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies {
compile externalDependency.grpcNettyShaded
compile externalDependency.grpcProtobuf
compile externalDependency.grpcStub
compile externalDependency.grpcServices
compile externalDependency.protobufJava
compile externalDependency.protobufJavaUtil
compile externalDependency.envoyApi
Expand Down
71 changes: 71 additions & 0 deletions d2/src/main/java/com/linkedin/d2/discovery/util/D2Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,77 @@ private static String getProp(String name, Properties sysProps, Map<String, Stri
return value;
}

/**
* Checks if the current Java version meets the minimum required version.
* Uses a simple approach similar to ComparableVersion but without external dependencies.
*
* @param currentVersion the current Java version string (e.g., "1.8.0_172", "11.0.1")
* @param minVersion the minimum required version string (e.g., "1.8.0_282-msft")
* @return true if the current version meets or exceeds the minimum requirement, false otherwise
*/
public static boolean isJavaVersionAtLeast(String currentVersion, String minVersion)
{
if (currentVersion == null || currentVersion.trim().isEmpty())
{
LOG.warn("Current Java version is null or empty");
return false;
}

if (minVersion == null || minVersion.trim().isEmpty())
{
LOG.warn("Minimum Java version is null or empty");
return true; // If no minimum specified, assume current is acceptable
}

try
{
// Simple approach: Java 9+ is always acceptable
if (isJava9OrHigher(currentVersion))
{
return true;
}

// For Java 8, check build number
if (currentVersion.startsWith("1.8.0_") && minVersion.startsWith("1.8.0_"))
{
int currentBuild = extractBuildNumber(currentVersion);
int minBuild = extractBuildNumber(minVersion);
return currentBuild >= minBuild;
}

// For other Java 8 formats or Java 7 and earlier
return false;
}
catch (NumberFormatException e)
{
LOG.warn("Failed to parse Java versions. Current: {}, Min: {}", currentVersion, minVersion, e);
return false;
}
}

/**
* Checks if the Java version is 9 or higher.
*/
private static boolean isJava9OrHigher(String version)
{
// Java 9+ versions start with "9.", "10.", "11.", etc.
return version.matches("^([9-9]|[1-9][0-9]+)\\..*");
}

/**
* Extracts build number from Java 8 version string.
* Example: "1.8.0_282-msft" -> 282
*/
private static int extractBuildNumber(String version)
{
String buildPart = version.substring("1.8.0_".length());
if (buildPart.contains("-"))
{
buildPart = buildPart.split("-")[0];
}
return Integer.parseInt(buildPart);
}

private D2Utils() {
// Utility class, no instantiation
}
Expand Down
128 changes: 128 additions & 0 deletions d2/src/main/java/com/linkedin/d2/xds/XdsClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.google.rpc.Code;
import com.linkedin.d2.discovery.util.D2Utils;
import com.linkedin.d2.jmx.NoOpXdsServerMetricsProvider;
import com.linkedin.d2.jmx.XdsClientJmx;
import com.linkedin.d2.jmx.XdsServerMetricsProvider;
Expand Down Expand Up @@ -74,6 +75,7 @@ public class XdsClientImpl extends XdsClient
new RateLimitedLogger(_log, TimeUnit.MINUTES.toMillis(1), SystemClock.instance());
public static final long DEFAULT_READY_TIMEOUT_MILLIS = 2000L;
public static final Integer DEFAULT_MAX_RETRY_BACKOFF_SECS = 30; // default value for max retry backoff seconds
private static final String MINIMUM_VALID_JAVA_VERSION = "1.8.0_282-msft";

/**
* The resource subscribers maps the resource type to its subscribers. Note that the {@link ResourceType#D2_URI}
Expand Down Expand Up @@ -307,6 +309,15 @@ void startRpcStreamLocal()
_log.warn("Tried to create duplicate RPC stream, ignoring!");
return;
}

if (!preCheckForIndisConnection())
Copy link
Contributor

@bohhyang bohhyang Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pre-check should be run only ONCE at startup, instead of in every startRpcStreamLocal (every reconnect). After #1093 is merged, there will be a start() method in XdsClientImpl, so you can add it there (cleaner).
Or add it to startRpcStream().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, this precheck shouldn't be run on the executor but should be blocking xds client startup, so that later we can control whether to fail the app startup if the precheck fails.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I add some logic to make sure the precheck will only run one-time

{
_log.error("Cannot start RPC stream, the pre check failed. "
+ "This can happen if the Java version is not supported or the xDS server is not reachable.");

}


AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceStub stub =
AggregatedDiscoveryServiceGrpc.newStub(_managedChannel);
AdsStream stream = new AdsStream(stub);
Expand Down Expand Up @@ -807,6 +818,123 @@ private void notifyStreamReconnect()
_xdsClientJmx.setIsConnected(true);
}




private boolean preCheckForIndisConnection()
{
try
{
String javaVersion = System.getProperty("java.version");
_log.info("Current Java version: {}", javaVersion);
if (!D2Utils.isJavaVersionAtLeast(javaVersion, MINIMUM_VALID_JAVA_VERSION))
{
_log.error("The current Java version {} is too low, please upgrade to at least {}, " +
"otherwise the service couldn't create grpc connection between service and INDIS," +
"check go/onboardindis Guidelines #2 for more details",
javaVersion,
MINIMUM_VALID_JAVA_VERSION);
return false;
}
// Check if we can actually reach the server at the given authority to rule out NACL issue
String serverAuthority = getXdsServerAuthority();
if (_managedChannel == null)
{
_log.error("[xds pre-check] Managed channel is null, cannot establish XDS connection to INDIS server");
return false;
}
if (serverAuthority == null || serverAuthority.isEmpty())
{
_log.error("[xds pre-check] Cannot determine INDIS xDS server authority, connection check failed");
return false;
}
_log.info("[xds pre-check] INDIS xDS server authority: {}", serverAuthority);

try
{
String[] parts = serverAuthority.split(":");
if (parts.length != 2)
{
_log.error("[xds pre-check] Invalid server authority format: {}, expected format: host:port", serverAuthority);
return false;
}

String host = parts[0];
int port = Integer.parseInt(parts[1]);

_log.info("[xds pre-check] Testing socket connection to INDIS xDS server: {}:{}", host, port);
// Check if we can connect to the server using a socket
try (java.net.Socket socket = new java.net.Socket())
{
socket.connect(new java.net.InetSocketAddress(host, port), (int) _readyTimeoutMillis);
_log.info("[xds pre-check] Successfully pinged to INDIS xDS server at {}:{}", host, port);
}
}
catch (Exception e)
{
_log.error("[xds pre-check] Failed to connect to INDIS xDS server at authority {}: {}, " +
"check go/onboardindis Guidelines #5 and #7 for more details",
serverAuthority, e.getMessage(), e);
return false;
}

// Check there is no connection issue for managed channel
try
{
io.grpc.health.v1.HealthGrpc.newBlockingStub(_managedChannel)
.withDeadlineAfter(_readyTimeoutMillis, java.util.concurrent.TimeUnit.MILLISECONDS)
.check(io.grpc.health.v1.HealthCheckRequest.newBuilder().setService("").build());
_log.info("[xds pre-check] Health check for managed channel passed - channel is SERVING");
}
catch (Exception e)
{
Throwable c = e;
while (c.getCause() != null) c = c.getCause();
String errorMessage = c.getClass().getName() + " | " + c.getMessage();
_log.error("[xds pre-check] Health check failed for managed channel: {}, " +
"check go/onboardindis Guidelines #2 and #9 for more details, " +
"if there is any other sslContext issue, please check with #pki team", errorMessage);
return false;
}

// check there is no protobuf version mismatch or excluding the io.envoyproxy module in build.gradle issue
try
{
Class.forName("com.google.protobuf.Descriptors$FileDescriptor");
_log.info("[xds pre-check] Protobuf Descriptor classes are available");

Class.forName("io.envoyproxy.envoy.service.discovery.v3.AggregatedDiscoveryServiceGrpc");
_log.info("[xds pre-check] Envoy API classes are available");
}
catch (ClassNotFoundException e)
{
_log.error("[xds pre-check] Required classes not found: {}, check go/onboardindis Guidelines #3 and #4",
e.getMessage());
return false;
}
catch (NoClassDefFoundError err)
{
_log.error("[xds pre-check] Class definition missing: {}, check go/onboardindis Guidelines #3 and #4",
err.getMessage(), err);
return false;
}
catch (Exception e)
{
_log.error("[xds pre-check] Unexpected exception: {}", e.getMessage(), e);
return false;
}
_log.info(
"[xds pre-check] All pre-checks for INDIS connection passed successfully, ready to start xDS RPC stream");
return true;
}
catch (Throwable t)
{
_log.error("[xds pre-check] Unexpected exception during pre-check: {}", t.getMessage(), t);
return false;
}
}


Map<String, ResourceSubscriber> getResourceSubscriberMap(ResourceType type)
{
return getResourceSubscribers().get(type);
Expand Down
87 changes: 87 additions & 0 deletions d2/src/test/java/com/linkedin/d2/discovery/util/TestD2Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,91 @@ public void testGetAppIdentityName(String sparkAppNameInSys, String appNameInSys
String appIdentityName = D2Utils.getAppIdentityName(props, env);
assertEquals(expectedAppIdentityName, appIdentityName);
}

@DataProvider(name = "provideJavaVersionTestData")
public Object[][] provideJavaVersionTestData()
{
return new Object[][]
{
// Test cases for Java 8 versions
{"1.8.0_282-msft", "1.8.0_282-msft", true}, // exact match
{"1.8.0_283-msft", "1.8.0_282-msft", true}, // higher build number
{"1.8.0_300", "1.8.0_282-msft", true}, // higher build number, no vendor
{"1.8.0_172", "1.8.0_282-msft", false}, // lower build number

// Test cases for Java 9+ versions (should always pass)
{"9.0.1", "1.8.0_282-msft", true}, // Java 9
{"10.0.1", "1.8.0_282-msft", true}, // Java 10
{"11.0.1", "1.8.0_282-msft", true}, // Java 11
{"17.0.1", "1.8.0_282-msft", true}, // Java 17
{"21.0.1", "1.8.0_282-msft", true}, // Java 21

// Test cases for edge cases
{"1.8.0_282-msft", null, true}, // no minimum version specified
{"1.8.0_282-msft", "", true}, // empty minimum version
{null, "1.8.0_282-msft", false}, // null current version
{"", "1.8.0_282-msft", false}, // empty current version
{" ", "1.8.0_282-msft", false}, // whitespace current version
{"1.8.0_282-msft", " ", true}, // whitespace minimum version

// Test cases for Java 7 and earlier (should fail)
{"1.7.0_80", "1.8.0_282-msft", false}, // Java 7
{"1.6.0_45", "1.8.0_282-msft", false}, // Java 6

// Test cases for invalid version formats
{"invalid-version", "1.8.0_282-msft", false}, // invalid current version
{"1.8.0_282-msft", "invalid-version", false}, // invalid minimum version
{"1.8.0", "1.8.0_282-msft", false}, // missing build number
{"1.8.0_", "1.8.0_282-msft", false}, // incomplete build number

// Test cases for different vendor suffixes
{"1.8.0_282-oracle", "1.8.0_282-msft", true}, // different vendor, same build
{"1.8.0_282-openjdk", "1.8.0_282-msft", true}, // different vendor, same build
{"1.8.0_282", "1.8.0_282-msft", true}, // no vendor vs vendor

// Test cases for very high build numbers
{"1.8.0_999", "1.8.0_282-msft", true}, // very high build number
{"1.8.0_282-msft", "1.8.0_999", false}, // very high minimum requirement
};
}

@Test(dataProvider = "provideJavaVersionTestData")
public void testIsJavaVersionAtLeast(String currentVersion, String minVersion, boolean expectedResult)
{
boolean result = D2Utils.isJavaVersionAtLeast(currentVersion, minVersion);
assertEquals("Failed for currentVersion: " + currentVersion + ", minVersion: " + minVersion,
expectedResult, result);
}

@Test
public void testIsJavaVersionAtLeastWithRealSystemProperty()
{
// Test with actual system property
String realJavaVersion = System.getProperty("java.version");
assertNotNull("java.version system property should not be null", realJavaVersion);

// Test that real version passes with a very low minimum requirement
boolean result = D2Utils.isJavaVersionAtLeast(realJavaVersion, "1.0.0");
assertTrue("Real Java version should pass with very low minimum requirement", result);

// Test that real version fails with a very high minimum requirement
result = D2Utils.isJavaVersionAtLeast(realJavaVersion, "99.0.0");
assertFalse("Real Java version should fail with very high minimum requirement", result);
}

@Test
public void testIsJavaVersionAtLeastWithSpecialCases()
{
// Test with whitespace trimming
assertTrue(D2Utils.isJavaVersionAtLeast(" 1.8.0_282-msft ", "1.8.0_282-msft"));
assertTrue(D2Utils.isJavaVersionAtLeast("1.8.0_282-msft", " 1.8.0_282-msft "));

// Test with very long vendor suffixes
assertTrue(D2Utils.isJavaVersionAtLeast("1.8.0_282-very-long-vendor-suffix", "1.8.0_282-msft"));
assertTrue(D2Utils.isJavaVersionAtLeast("1.8.0_283-very-long-vendor-suffix", "1.8.0_282-msft"));

// Test with multiple dashes in vendor suffix
assertTrue(D2Utils.isJavaVersionAtLeast("1.8.0_282-msft-preview", "1.8.0_282-msft"));
assertTrue(D2Utils.isJavaVersionAtLeast("1.8.0_283-msft-preview", "1.8.0_282-msft"));
}
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version=29.74.3
version=29.74.4
group=com.linkedin.pegasus
org.gradle.configureondemand=true
org.gradle.parallel=true
Expand Down