diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java index 7fe8d7363b5e..f1ea462ca638 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ClientConfigDiagnosticsTest.java @@ -11,6 +11,7 @@ import com.azure.cosmos.CosmosRegionSwitchHint; import com.azure.cosmos.SessionRetryOptions; import com.azure.cosmos.SessionRetryOptionsBuilder; +import com.azure.cosmos.implementation.clienttelemetry.ClientTelemetry; import com.azure.cosmos.implementation.perPartitionCircuitBreaker.PartitionLevelCircuitBreakerConfig; import com.azure.cosmos.implementation.directconnectivity.RntbdTransportClient; import com.azure.cosmos.implementation.guava25.collect.ImmutableList; @@ -40,6 +41,8 @@ import static org.assertj.core.api.Assertions.assertThat; public class ClientConfigDiagnosticsTest { + private static final String vmInstanceMachineId = ClientTelemetry.getMachineId(null); + private final ObjectMapper objectMapper = new ObjectMapper(); private static final ImplementationBridgeHelpers.CosmosContainerIdentityHelper.CosmosContainerIdentityAccessor containerIdentityAccessor = ImplementationBridgeHelpers .CosmosContainerIdentityHelper @@ -165,7 +168,7 @@ public void bareMinimum() throws Exception { ObjectNode objectNode = (ObjectNode) objectMapper.readTree(jsonWriter.toString()); assertThat(objectNode.get("id").asInt()).isEqualTo(1); - assertThat(objectNode.get("machineId").asText()).isEqualTo(machineId); + assertThat(objectNode.get("machineId").asText()).isEqualTo(Strings.isNullOrEmpty(vmInstanceMachineId) ? machineId : vmInstanceMachineId); assertThat(objectNode.get("numberOfClients").asInt()).isEqualTo(2); assertThat(objectNode.get("consistencyCfg").asText()).isEqualTo("(consistency: null, readConsistencyStrategy: null, mm: false, prgns: [null])"); assertThat(objectNode.get("connCfg").get("rntbd").asText()).isEqualTo("null"); @@ -198,7 +201,7 @@ public void rntbd() throws Exception { ObjectNode objectNode = (ObjectNode) objectMapper.readTree(jsonWriter.toString()); assertThat(objectNode.get("id").asInt()).isEqualTo(1); - assertThat(objectNode.get("machineId").asText()).isEqualTo(machineId); + assertThat(objectNode.get("machineId").asText()).isEqualTo(Strings.isNullOrEmpty(vmInstanceMachineId) ? machineId : vmInstanceMachineId); assertThat(objectNode.get("numberOfClients").asInt()).isEqualTo(2); assertThat(objectNode.get("consistencyCfg").asText()).isEqualTo("(consistency: null, readConsistencyStrategy: null, mm: false, prgns: [null])"); assertThat(objectNode.get("connCfg").get("rntbd").asText()).isEqualTo("(cto:PT5S, nrto:PT5S, icto:PT0S, ieto:PT1H, mcpe:130, mrpc:30, cer:true)"); @@ -235,7 +238,7 @@ public void gw() throws Exception { ObjectNode objectNode = (ObjectNode) objectMapper.readTree(jsonWriter.toString()); assertThat(objectNode.get("id").asInt()).isEqualTo(1); - assertThat(objectNode.get("machineId").asText()).isEqualTo(machineId); + assertThat(objectNode.get("machineId").asText()).isEqualTo(Strings.isNullOrEmpty(vmInstanceMachineId) ? machineId : vmInstanceMachineId); assertThat(objectNode.get("numberOfClients").asInt()).isEqualTo(2); assertThat(objectNode.get("consistencyCfg").asText()).isEqualTo("(consistency: null, readConsistencyStrategy: null, mm: false, prgns: [null])"); assertThat(objectNode.get("connCfg").get("rntbd").asText()).isEqualTo("null"); @@ -309,7 +312,7 @@ public void full( ObjectNode objectNode = (ObjectNode) objectMapper.readTree(jsonWriter.toString()); assertThat(objectNode.get("id").asInt()).isEqualTo(1); - assertThat(objectNode.get("machineId").asText()).isEqualTo(machineId); + assertThat(objectNode.get("machineId").asText()).isEqualTo(Strings.isNullOrEmpty(vmInstanceMachineId) ? machineId : vmInstanceMachineId); assertThat(objectNode.get("numberOfClients").asInt()).isEqualTo(2); assertThat(objectNode.get("consistencyCfg").asText()).isEqualTo("(consistency: null, readConsistencyStrategy: null, mm: false, prgns: [westus1,westus2])"); assertThat(objectNode.get("connCfg").get("rntbd").asText()).isEqualTo("null"); @@ -362,7 +365,7 @@ public void sessionRetryOptionsInDiagnostics(SessionRetryOptions sessionRetryOpt ObjectNode objectNode = (ObjectNode) objectMapper.readTree(jsonWriter.toString()); assertThat(objectNode.get("id").asInt()).isEqualTo(1); - assertThat(objectNode.get("machineId").asText()).isEqualTo(machineId); + assertThat(objectNode.get("machineId").asText()).isEqualTo(Strings.isNullOrEmpty(vmInstanceMachineId) ? machineId : vmInstanceMachineId); assertThat(objectNode.get("numberOfClients").asInt()).isEqualTo(2); assertThat(objectNode.get("consistencyCfg").asText()).isEqualTo("(consistency: null, readConsistencyStrategy: null, mm: false, prgns: [null])"); assertThat(objectNode.get("connCfg").get("rntbd").asText()).isEqualTo("null"); diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ImplementationBridgeHelpersTest.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ImplementationBridgeHelpersTest.java index e084e6dd2743..24a448c54b84 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ImplementationBridgeHelpersTest.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ImplementationBridgeHelpersTest.java @@ -285,6 +285,106 @@ public static void main(String[] args) { } } + /** + * Regression test for the ClientTelemetry static-init failure when IMDS access is disabled. + * A fresh JVM is required so the system property is in effect before any Cosmos classes load. + */ + @Test(groups = { "unit" }) + public void cosmosClientBuildShouldSucceedWhenImdsAccessIsDisabled() throws Exception { + String javaHome = System.getProperty("java.home"); + String javaBin = javaHome + java.io.File.separator + "bin" + java.io.File.separator + "java"; + String classpath = System.getProperty("java.class.path"); + + List command = new ArrayList<>(); + command.add(javaBin); + command.add("-DCOSMOS.DISABLE_IMDS_ACCESS=true"); + + try { + int majorVersion = Integer.parseInt(System.getProperty("java.specification.version").split("\\.")[0]); + if (majorVersion >= 9) { + command.add("--add-opens"); + command.add("java.base/java.lang=ALL-UNNAMED"); + } + } catch (NumberFormatException e) { + // JDK 8 + } + + command.add("-cp"); + command.add(classpath); + command.add(ClientTelemetryImdsDisabledChildProcess.class.getName()); + + ProcessBuilder pb = new ProcessBuilder(command); + pb.redirectErrorStream(true); + Process process = pb.start(); + + StringBuilder output = new StringBuilder(); + Thread gobbler = new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append(System.lineSeparator()); + logger.info("[client-telemetry-imds-disabled] {}", line); + } + } catch (Exception e) { + // Process destroyed + } + }); + gobbler.setDaemon(true); + gobbler.start(); + + boolean completed = process.waitFor(60, TimeUnit.SECONDS); + if (!completed) { + process.destroyForcibly(); + gobbler.join(5000); + fail("ClientTelemetry IMDS-disabled regression check timed out after 60s. Output:\n" + output); + } + + gobbler.join(5000); + int exitCode = process.exitValue(); + assertThat(exitCode) + .as("Child JVM exited with non-zero code. Output:\n" + output) + .isEqualTo(0); + } + + public static final class ClientTelemetryImdsDisabledChildProcess { + public static void main(String[] args) { + try { + com.azure.cosmos.CosmosAsyncClient client = new com.azure.cosmos.CosmosClientBuilder() + .endpoint(TestConfigurations.HOST) + .key(TestConfigurations.MASTER_KEY) + .buildAsyncClient(); + + client.close(); + System.out.println("Client built successfully with IMDS access disabled."); + System.exit(0); + } catch (Throwable throwable) { + if (containsClientTelemetryStaticInitFailure(throwable)) { + throwable.printStackTrace(System.err); + System.exit(1); + } + + System.out.println("Client reached the expected network/auth path without a ClientTelemetry static-init failure."); + throwable.printStackTrace(System.out); + System.exit(0); + } + } + + private static boolean containsClientTelemetryStaticInitFailure(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof ExceptionInInitializerError + || current instanceof NoClassDefFoundError + || (current.getMessage() != null && current.getMessage().contains("ClientTelemetry"))) { + return true; + } + + current = current.getCause(); + } + + return false; + } + } + /** * Verifies that every {@code *Helper} inner class in * {@link ImplementationBridgeHelpers} has a resolvable accessor — i.e., calling diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index 8cbf52e3f916..c17648b95294 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -12,6 +12,7 @@ * Fixed JVM `` deadlock when multiple threads concurrently trigger Cosmos SDK class loading for the first time. - See [PR 48689](https://github.com/Azure/azure-sdk-for-java/pull/48689) * Fixed an issue where `CustomItemSerializer` was incorrectly applied to internal SDK query pipeline structures (e.g., `OrderByRowResult`, `Document`), causing deserialization failures in ORDER BY, GROUP BY, aggregate, DISTINCT, and hybrid search queries. - See [PR 48811](https://github.com/Azure/azure-sdk-for-java/pull/48811) * Fixed an issue where `SqlParameter` ignored the configured `CustomItemSerializer`, always using the internal default serializer instead. - See [PR 48811](https://github.com/Azure/azure-sdk-for-java/pull/48811) +* Fixed a `ClientTelemetry` static initialization failure when IMDS access is disabled, preventing `NoClassDefFoundError` during Cosmos client creation in non-Azure environments. - See [PR 48888](https://github.com/Azure/azure-sdk-for-java/pull/48888) #### Other Changes diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/clienttelemetry/ClientTelemetry.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/clienttelemetry/ClientTelemetry.java index f5f4f5797689..79cad5d1c9da 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/clienttelemetry/ClientTelemetry.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/clienttelemetry/ClientTelemetry.java @@ -51,17 +51,29 @@ private static ImplementationBridgeHelpers.CosmosClientTelemetryConfigHelper.Cos // - The fetch executes at most once // - All concurrent subscribers share the single result // - The HTTP client is created and disposed within the fetch - private static final Mono CACHED_METADATA = fetchAzureVmMetadata().cache(); + private static final Mono CACHED_METADATA; // Sentinel for "not on Azure VM" or "IMDS unreachable" - private static final AzureVMMetadata METADATA_NOT_AVAILABLE = new AzureVMMetadata(); + private static final AzureVMMetadata METADATA_NOT_AVAILABLE; // IMDS Constants - private static final String IMDS_AZURE_VM_METADATA = "http://169.254.169.254:80/metadata/instance?api-version=2020-06-01"; - private static final Duration IMDS_DEFAULT_NETWORK_REQUEST_TIMEOUT = Duration.ofSeconds(5); - private static final Duration IMDS_DEFAULT_IDLE_CONNECTION_TIMEOUT = Duration.ofSeconds(60); - private static final Duration IMDS_DEFAULT_CONNECTION_ACQUIRE_TIMEOUT = Duration.ofSeconds(5); - private static final int IMDS_DEFAULT_MAX_CONNECTION_POOL_SIZE = 5; + private static final String IMDS_AZURE_VM_METADATA; + private static final Duration IMDS_DEFAULT_NETWORK_REQUEST_TIMEOUT; + private static final Duration IMDS_DEFAULT_IDLE_CONNECTION_TIMEOUT; + private static final Duration IMDS_DEFAULT_CONNECTION_ACQUIRE_TIMEOUT; + private static final int IMDS_DEFAULT_MAX_CONNECTION_POOL_SIZE; + + static { + // Initialize the sentinel and IMDS defaults before creating the cached Mono, + // because fetchAzureVmMetadata() reads them during class initialization. + METADATA_NOT_AVAILABLE = new AzureVMMetadata(); + IMDS_AZURE_VM_METADATA = "http://169.254.169.254:80/metadata/instance?api-version=2020-06-01"; + IMDS_DEFAULT_NETWORK_REQUEST_TIMEOUT = Duration.ofSeconds(5); + IMDS_DEFAULT_IDLE_CONNECTION_TIMEOUT = Duration.ofSeconds(60); + IMDS_DEFAULT_CONNECTION_ACQUIRE_TIMEOUT = Duration.ofSeconds(5); + IMDS_DEFAULT_MAX_CONNECTION_POOL_SIZE = 5; + CACHED_METADATA = fetchAzureVmMetadata().cache(); + } // Per-instance fields private final ClientTelemetryInfo clientTelemetryInfo;