diff --git a/common/perf-test-core/pom.xml b/common/perf-test-core/pom.xml index fe4afb60e18db..1cb0e1a1c4661 100644 --- a/common/perf-test-core/pom.xml +++ b/common/perf-test-core/pom.xml @@ -56,7 +56,7 @@ com.beust:jcommander:[1.82] io.vertx:vertx-codegen:[4.5.32] - io.projectreactor:reactor-core:[3.7.19] + io.projectreactor:reactor-core:[3.8.7] diff --git a/docs/contributor/building.md b/docs/contributor/building.md index e443dbd588a32..e62ca997407f9 100644 --- a/docs/contributor/building.md +++ b/docs/contributor/building.md @@ -47,6 +47,26 @@ mvn install -f sdk/appconfiguration/pom.xml -Dgpg.skip -Drevapi.skip -DskipTests Remove `-DskipTests` from the build commands above. Remove `-Djacoco.skip` as well if you want coverage reports. Spring integration tests have a separate switch, `-DskipSpringITs=false`, and require their own test setup. +### Test-only Java module dependencies + +On JDK 9 and later, tests run as part of their library's named module. When tests directly use classes +from a dependency that the production module does not read, add a narrow `--add-reads` option to the +package's `javaModulesSurefireArgLine` property. Failsafe inherits this property through +`javaModulesFailsafeArgLine`. + +For example, tests using Netty's `HttpResponseStatus` need to read the explicit `io.netty.codec.http` +module in Netty 4.2: + +```xml + + --add-reads com.azure.ai.documentintelligence=io.netty.codec.http + +``` + +Keep these test-only reads out of production `module-info.java` descriptors. Preserve any existing +options in the property, and add reads for each module directly used by the tests. These options are +only applied by the Java 9+ build profile and do not affect Java 8 test runs. + ### Build selected client libraries Select libraries from the root reactor by their Maven coordinates. For example, build App Configuration diff --git a/eng/pipelines/templates/jobs/ci.yml b/eng/pipelines/templates/jobs/ci.yml index 1b0af4b0c28cb..463315065b850 100644 --- a/eng/pipelines/templates/jobs/ci.yml +++ b/eng/pipelines/templates/jobs/ci.yml @@ -308,6 +308,7 @@ jobs: - job: 'Analyze' condition: and(succeeded(), ne(variables['Skip.Analyze'], 'true')) + timeoutInMinutes: 120 pool: name: $(LINUXPOOL) diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 8a385ea84b449..0d4bd6af14c10 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -44,22 +44,25 @@ commons-net:commons-net;3.9.0 io.cloudevents:cloudevents-api;2.2.0 io.cloudevents:cloudevents-core;2.2.0 io.fabric8:kubernetes-client;6.12.1 -io.netty:netty-buffer;4.1.138.Final -io.netty:netty-common;4.1.138.Final -io.netty:netty-codec;4.1.138.Final -io.netty:netty-codec-http;4.1.138.Final -io.netty:netty-codec-http2;4.1.138.Final -io.netty:netty-handler;4.1.138.Final -io.netty:netty-handler-proxy;4.1.138.Final -io.netty:netty-resolver;4.1.138.Final -io.netty:netty-resolver-dns;4.1.138.Final +io.netty:netty-buffer;4.2.19.Final +io.netty:netty-common;4.2.19.Final +io.netty:netty-codec-base;4.2.19.Final +io.netty:netty-codec-http;4.2.19.Final +io.netty:netty-codec-http2;4.2.19.Final +io.netty:netty-codec-http3;4.2.19.Final +io.netty:netty-handler;4.2.19.Final +io.netty:netty-handler-proxy;4.2.19.Final +io.netty:netty-resolver;4.2.19.Final +io.netty:netty-resolver-dns;4.2.19.Final +io.netty:netty-resolver-dns-native-macos;4.2.19.Final io.netty:netty-tcnative-boringssl-static;2.0.84.Final -io.netty:netty-transport;4.1.138.Final -io.netty:netty-transport-native-epoll;4.1.138.Final -io.netty:netty-transport-native-unix-common;4.1.138.Final -io.netty:netty-transport-native-kqueue;4.1.138.Final -io.projectreactor.netty:reactor-netty-http;1.2.18 -io.projectreactor:reactor-core;3.7.19 +io.netty:netty-transport;4.2.19.Final +io.netty:netty-transport-classes-epoll;4.2.19.Final +io.netty:netty-transport-native-epoll;4.2.19.Final +io.netty:netty-transport-native-unix-common;4.2.19.Final +io.netty:netty-transport-native-kqueue;4.2.19.Final +io.projectreactor.netty:reactor-netty-http;1.3.7 +io.projectreactor:reactor-core;3.8.7 io.vertx:vertx-codegen;4.5.32 io.vertx:vertx-core;4.5.32 javax.websocket:javax.websocket-api;1.1 @@ -124,7 +127,7 @@ io.opentelemetry:opentelemetry-bom;1.65.0 io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry;2.31.1-alpha io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter;2.31.1 io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0;2.31.1-alpha -io.projectreactor:reactor-test;3.7.19 +io.projectreactor:reactor-test;3.8.7 io.github.hakky54:logcaptor;2.9.3 com.squareup.okio:okio;3.18.1 com.squareup.okio:okio-jvm;3.18.1 @@ -205,7 +208,7 @@ org.revapi:revapi-reporter-json;0.5.1 org.revapi:revapi-maven-plugin;0.15.1 # External Dependency Exceptions -# This section is for external dependencies whose versions were different than +# This section is for external dependencies whose versions were different from # what was defined in the parent pom. # Format; # _groupId:artifactId;dependency-version diff --git a/eng/versioning/supported_external_dependency_versions.json b/eng/versioning/supported_external_dependency_versions.json index 35fddef80f79f..722d67abfd9ee 100644 --- a/eng/versioning/supported_external_dependency_versions.json +++ b/eng/versioning/supported_external_dependency_versions.json @@ -162,10 +162,10 @@ ], "reactor_2025": [ - { "io.projectreactor.netty:reactor-netty": "1.3.0-M2" }, - { "io.projectreactor.netty:reactor-netty-http": "1.3.0-M2" }, - { "io.projectreactor:reactor-core": "3.8.0-M2" }, - { "io.projectreactor:reactor-test": "3.8.0-M2" } + { "io.projectreactor.netty:reactor-netty": "1.3.0" }, + { "io.projectreactor.netty:reactor-netty-http": "1.3.0" }, + { "io.projectreactor:reactor-core": "3.8.0" }, + { "io.projectreactor:reactor-test": "3.8.0" } ], "reactor_2024": [ diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 6d7c93457e022..8be67a77ef068 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -579,6 +579,9 @@ io.clientcore:optional-dependency-tests;1.0.0-beta.1;1.0.0-beta.1 # In the pom, the version update tag after the version should name the unreleased package and the dependency version: # +unreleased_com.azure:azure-core;1.61.0-beta.1 +unreleased_com.azure:azure-core-http-netty;1.17.0-beta.1 +unreleased_com.azure:azure-core-test;1.27.0-beta.20 unreleased_com.azure.v2:azure-core;2.0.0-beta.1 unreleased_com.azure.v2:azure-identity;2.0.0-beta.1 unreleased_com.azure.v2:azure-data-appconfiguration;2.0.0-beta.1 diff --git a/sdk/ai/azure-ai-agents/pom.xml b/sdk/ai/azure-ai-agents/pom.xml index f513ad6239e17..d35dcf69ec983 100644 --- a/sdk/ai/azure-ai-agents/pom.xml +++ b/sdk/ai/azure-ai-agents/pom.xml @@ -63,42 +63,42 @@ com.azure azure-core - 1.60.0 + 1.61.0-beta.1 com.azure azure-core-http-netty - 1.16.8 + 1.17.0-beta.1 io.projectreactor.netty reactor-netty-http - 1.2.18 + 1.3.7 io.netty netty-codec-http - 4.1.138.Final + 4.2.19.Final io.netty netty-transport - 4.1.138.Final + 4.2.19.Final io.netty netty-common - 4.1.138.Final + 4.2.19.Final io.netty - netty-codec - 4.1.138.Final + netty-codec-base + 4.2.19.Final io.netty netty-buffer - 4.1.138.Final + 4.2.19.Final com.squareup.okhttp3 @@ -133,7 +133,7 @@ com.azure azure-core-test - 1.27.0-beta.19 + 1.27.0-beta.20 test @@ -150,12 +150,12 @@ com.openai:openai-java:[4.69.0] - io.projectreactor.netty:reactor-netty-http:[1.2.18] - io.netty:netty-codec-http:[4.1.138.Final] - io.netty:netty-transport:[4.1.138.Final] - io.netty:netty-common:[4.1.138.Final] - io.netty:netty-codec:[4.1.138.Final] - io.netty:netty-buffer:[4.1.138.Final] + io.projectreactor.netty:reactor-netty-http:[1.3.7] + io.netty:netty-codec-http:[4.2.19.Final] + io.netty:netty-transport:[4.2.19.Final] + io.netty:netty-common:[4.2.19.Final] + io.netty:netty-codec-base:[4.2.19.Final] + io.netty:netty-buffer:[4.2.19.Final] com.squareup.okhttp3:okhttp:[4.12.0] com.squareup.okio:okio-jvm:[3.18.1] diff --git a/sdk/ai/azure-ai-agents/src/main/java/module-info.java b/sdk/ai/azure-ai-agents/src/main/java/module-info.java index 06498120ce9ef..c3d5364775d4e 100644 --- a/sdk/ai/azure-ai-agents/src/main/java/module-info.java +++ b/sdk/ai/azure-ai-agents/src/main/java/module-info.java @@ -12,6 +12,7 @@ requires io.netty.transport; requires io.netty.common; requires io.netty.codec; + requires io.netty.codec.compression; requires io.netty.buffer; requires okhttp3; requires okio; diff --git a/sdk/attestation/azure-security-attestation/pom.xml b/sdk/attestation/azure-security-attestation/pom.xml index cf049fb426662..2900e9b032d01 100644 --- a/sdk/attestation/azure-security-attestation/pom.xml +++ b/sdk/attestation/azure-security-attestation/pom.xml @@ -37,6 +37,7 @@ --add-opens com.azure.security.attestation/com.azure.security.attestation=ALL-UNNAMED --add-opens com.azure.security.attestation/com.azure.security.attestation.models=ALL-UNNAMED + --add-reads com.azure.security.attestation=org.bouncycastle.provider false diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/client/JdkHttpClientBuilderIT.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/client/JdkHttpClientBuilderIT.java index c870f5b6cd289..715dae89efe8f 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/client/JdkHttpClientBuilderIT.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/client/JdkHttpClientBuilderIT.java @@ -88,6 +88,7 @@ public void buildWithHttpProxyFromEnvConfiguration() throws IOException { Configuration configuration = Configuration.from(new TestConfigurationSource() .put(Configuration.HTTP_PROXY, "http://" + PROXY_USER_INFO + proxyEndpoint.getHost() + ":" + proxyEndpoint.getPort()) + .put(Configuration.HTTPS_PROXY, "") .put("java.net.useSystemProxies", "true")); HttpClient httpClient = new JdkHttpClientBuilder().configuration(configuration).build(); diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/ProxyOptionsTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/ProxyOptionsTests.java index 152e59e297852..9617412cd76d5 100644 --- a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/ProxyOptionsTests.java +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/ProxyOptionsTests.java @@ -234,53 +234,35 @@ public void invalidHttpPortEnvironmentConfigDefault(String port) { private static Stream loadFromEnvironmentSupplier() { return Stream.of( // Basic Azure HTTPS proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.HTTPS_PROXY, AZURE_HTTPS_PROXY_HOST_ONLY)), - PROXY_HOST, 443, null, null, null), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTPS_PROXY_HOST_ONLY, null, true), PROXY_HOST, 443, + null, null, null), // Username only Azure HTTPS proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.HTTPS_PROXY, AZURE_HTTPS_PROXY_WITH_USERNAME)), - PROXY_HOST, 443, null, null, null), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTPS_PROXY_WITH_USERNAME, null, true), PROXY_HOST, + 443, null, null, null), // Complete Azure HTTPS proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.HTTPS_PROXY, AZURE_HTTPS_PROXY_WITH_USER_AND_PASS)), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTPS_PROXY_WITH_USER_AND_PASS, null, true), PROXY_HOST, 443, FAKE_PROXY_USER_PLACEHOLDER, FAKE_PROXY_PASSWORD_PLACEHOLDER, null), // Azure HTTPS proxy with non-proxying hosts. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.HTTPS_PROXY, AZURE_HTTPS_PROXY_HOST_ONLY) - .put(Configuration.NO_PROXY, NON_PROXY_HOSTS)), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTPS_PROXY_HOST_ONLY, NON_PROXY_HOSTS, true), PROXY_HOST, 443, null, null, "(" + NON_PROXY_HOSTS + ")"), // Basic Azure HTTP proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.HTTP_PROXY, AZURE_HTTP_PROXY_HOST_ONLY)), - PROXY_HOST, 80, null, null, null), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTP_PROXY_HOST_ONLY, null, false), PROXY_HOST, 80, + null, null, null), // Username only Azure HTTP proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.HTTP_PROXY, AZURE_HTTP_PROXY_WITH_USERNAME)), - PROXY_HOST, 80, null, null, null), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTP_PROXY_WITH_USERNAME, null, false), PROXY_HOST, + 80, null, null, null), // Complete Azure HTTP proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.HTTP_PROXY, AZURE_HTTP_PROXY_WITH_USER_AND_PASS)), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTP_PROXY_WITH_USER_AND_PASS, null, false), PROXY_HOST, 80, FAKE_PROXY_USER_PLACEHOLDER, FAKE_PROXY_PASSWORD_PLACEHOLDER, null), // Azure HTTP proxy with non-proxying hosts. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.HTTP_PROXY, AZURE_HTTP_PROXY_HOST_ONLY) - .put(Configuration.NO_PROXY, NON_PROXY_HOSTS)), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTP_PROXY_HOST_ONLY, NON_PROXY_HOSTS, false), PROXY_HOST, 80, null, null, "(" + NON_PROXY_HOSTS + ")"), /* @@ -442,10 +424,8 @@ private static Stream nonProxyHostsSupplier() { /* * This emulates loading an environment formatted proxy. */ - TestConfigurationSource envSource - = new TestConfigurationSource().put(Configuration.HTTPS_PROXY, "http://localhost:7777") - .put(Configuration.NO_PROXY, noProxyNonProxyHosts); - Configuration environmentProxyConfiguration = setJavaSystemProxyPrerequisiteToTrue(envSource); + Configuration environmentProxyConfiguration + = setJavaSystemProxyPrerequisiteToTrue("http://localhost:7777", noProxyNonProxyHosts, true); Pattern environmentProxyConfigurationPattern = compile(fromConfiguration(environmentProxyConfiguration).getNonProxyHosts(), Pattern.CASE_INSENSITIVE); @@ -505,7 +485,22 @@ public void sanitizeJavaHttpNonProxyHostsDoesNotSplitEscapedPipes() { assertEquals("(" + nonProxyHosts + ")", ProxyOptions.sanitizeNoProxy(nonProxyHosts)); } - private static Configuration setJavaSystemProxyPrerequisiteToTrue(TestConfigurationSource source) { - return Configuration.from(EMPTY_SOURCE, EMPTY_SOURCE, source.put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true")); + private static Configuration setJavaSystemProxyPrerequisiteToTrue(String proxy, String nonProxyHosts, + boolean isHttps) { + TestConfigurationSource testSource = new TestConfigurationSource().put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true") + // Use empty string when non-proxy hosts is null or empty to ensure that the environment configuration + // doesn't attempt to fall back to loading from System.properties or System.env. + .put(Configuration.NO_PROXY, CoreUtils.isNullOrEmpty(nonProxyHosts) ? "" : nonProxyHosts); + + if (isHttps) { + testSource.put(Configuration.HTTPS_PROXY, proxy); + } else { + testSource.put(Configuration.HTTP_PROXY, proxy) + // Same as with non-proxy hosts, set HTTPS_PROXY to empty when testing HTTP_PROXY as HTTPS is checked + // first and could fall back to loading from System.properties or System.env. + .put(Configuration.HTTPS_PROXY, ""); + } + + return Configuration.from(EMPTY_SOURCE, EMPTY_SOURCE, testSource); } } diff --git a/sdk/clientcore/cspell.yml b/sdk/clientcore/cspell.yml index 1874e7c0202fc..a285a56a850db 100644 --- a/sdk/clientcore/cspell.yml +++ b/sdk/clientcore/cspell.yml @@ -14,6 +14,7 @@ words: - "erroronmissingdir" - "fqcn" - "httpheadename" + - "jpms" - "lmolkova" - "replayablity" - "serializables" diff --git a/sdk/clientcore/http-netty4/CHANGELOG.md b/sdk/clientcore/http-netty4/CHANGELOG.md index 13dd08af78abe..98163b4239585 100644 --- a/sdk/clientcore/http-netty4/CHANGELOG.md +++ b/sdk/clientcore/http-netty4/CHANGELOG.md @@ -9,3 +9,7 @@ ### Bugs Fixed ### Other Changes + +- Upgraded Netty to `4.2.19.Final`, using I/O handler factories for event loops while preserving pooled buffer allocation + unless `io.netty.allocator.type` is explicitly configured. +- Preserved HTTP/2 response decompression with Netty's separate compression module when using Java modules. diff --git a/sdk/clientcore/http-netty4/README.md b/sdk/clientcore/http-netty4/README.md index 3007dd75d1408..0ddb20b9b73f8 100644 --- a/sdk/clientcore/http-netty4/README.md +++ b/sdk/clientcore/http-netty4/README.md @@ -20,6 +20,42 @@ Client Core Netty HTTP client is a plugin for the `io.clientcore.core` HTTP clie ## Key concepts +### Native transports on the module path + +On JDK 11 or later, Netty 4.2's epoll/kqueue transport classes and platform-specific JNI libraries +are **separate explicit Java modules**. Putting their JARs on the module path does not resolve them +automatically. When a named-module application wants `http-netty4` to select a native transport, +add a matching Netty native classifier as a **runtime** dependency; this package only declares the +native classifiers as test dependencies. For example, on Linux x86_64: + +```xml + + io.netty + netty-transport-native-epoll + ${netty.version} + linux-x86_64 + +``` + +Define `${netty.version}` in your POM or replace it with the Netty version aligned with this client. +The native classifier dependency also brings in +the corresponding `netty-transport-classes-*` JAR. Resolve **both** the class and native-resource +modules when launching the application: + +| Platform and classifier | Launch option | +| --- | --- | +| Linux x86_64 (`netty-transport-native-epoll:linux-x86_64`) | `--add-modules=io.netty.transport.classes.epoll,io.netty.transport.epoll.linux.x86_64` | +| macOS x86_64 (`netty-transport-native-kqueue:osx-x86_64`) | `--add-modules=io.netty.transport.classes.kqueue,io.netty.transport.kqueue.osx.x86_64` | + +For other architectures, supply the matching native classifier and use its module name as reported by +`jar --describe-module --file --release 11`; see +[Netty's native transport guide](https://netty.io/wiki/native-transports.html#using-the-native-transports) +for classifier details. Resolving only the class module does not +expose the bundled JNI library; adding `requires static` to a library descriptor does not make +either module a runtime root. Without usable native transport the client falls back to JDK NIO. +Neither Java 8 nor classpath applications need these JPMS launch options. Module resolution by +itself does not establish that native I/O works on the current OS. + ## Examples ### Create a Simple Client diff --git a/sdk/clientcore/http-netty4/pom.xml b/sdk/clientcore/http-netty4/pom.xml index 31bf16223b97f..69bc62ffad745 100644 --- a/sdk/clientcore/http-netty4/pom.xml +++ b/sdk/clientcore/http-netty4/pom.xml @@ -56,7 +56,7 @@ io.clientcore.core.implementation*,io.clientcore.core.models,io.clientcore.core.util,io.clientcore.core.util* - 4.1.138.Final + 4.2.19.Final @@ -69,52 +69,52 @@ io.netty netty-buffer - 4.1.138.Final + 4.2.19.Final io.netty - netty-codec - 4.1.138.Final + netty-codec-base + 4.2.19.Final io.netty netty-codec-http - 4.1.138.Final + 4.2.19.Final io.netty netty-codec-http2 - 4.1.138.Final + 4.2.19.Final io.netty netty-common - 4.1.138.Final + 4.2.19.Final io.netty netty-handler - 4.1.138.Final + 4.2.19.Final io.netty netty-handler-proxy - 4.1.138.Final + 4.2.19.Final io.netty netty-resolver - 4.1.138.Final + 4.2.19.Final io.netty netty-resolver-dns - 4.1.138.Final + 4.2.19.Final io.netty netty-transport - 4.1.138.Final + 4.2.19.Final @@ -126,32 +126,18 @@ test - - io.netty - netty-transport-native-unix-common - 4.1.138.Final - linux-x86_64 - test - io.netty netty-transport-native-epoll - 4.1.138.Final + 4.2.19.Final linux-x86_64 test - - io.netty - netty-transport-native-unix-common - 4.1.138.Final - osx-x86_64 - test - io.netty netty-transport-native-kqueue - 4.1.138.Final + 4.2.19.Final osx-x86_64 test @@ -223,16 +209,16 @@ - io.netty:netty-buffer:[4.1.138.Final] - io.netty:netty-codec:[4.1.138.Final] - io.netty:netty-codec-http:[4.1.138.Final] - io.netty:netty-codec-http2:[4.1.138.Final] - io.netty:netty-common:[4.1.138.Final] - io.netty:netty-handler:[4.1.138.Final] - io.netty:netty-handler-proxy:[4.1.138.Final] - io.netty:netty-resolver:[4.1.138.Final] - io.netty:netty-resolver-dns:[4.1.138.Final] - io.netty:netty-transport:[4.1.138.Final] + io.netty:netty-buffer:[4.2.19.Final] + io.netty:netty-codec-base:[4.2.19.Final] + io.netty:netty-codec-http:[4.2.19.Final] + io.netty:netty-codec-http2:[4.2.19.Final] + io.netty:netty-common:[4.2.19.Final] + io.netty:netty-handler:[4.2.19.Final] + io.netty:netty-handler-proxy:[4.2.19.Final] + io.netty:netty-resolver:[4.2.19.Final] + io.netty:netty-resolver-dns:[4.2.19.Final] + io.netty:netty-transport:[4.2.19.Final] diff --git a/sdk/clientcore/http-netty4/spotbugs-exclude.xml b/sdk/clientcore/http-netty4/spotbugs-exclude.xml index 57b36d11db672..3ab9dae484d83 100644 --- a/sdk/clientcore/http-netty4/spotbugs-exclude.xml +++ b/sdk/clientcore/http-netty4/spotbugs-exclude.xml @@ -62,6 +62,10 @@ + + + + diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClientBuilder.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClientBuilder.java index 68c163abd0014..ef38cddf23eeb 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClientBuilder.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/NettyHttpClientBuilder.java @@ -12,15 +12,22 @@ import io.clientcore.http.netty4.implementation.ChannelInitializationProxyHandler; import io.clientcore.http.netty4.implementation.Netty4ConnectionPool; import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.IoEventLoopGroup; +import io.netty.channel.IoHandler; +import io.netty.channel.IoHandlerFactory; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.internal.SystemPropertyUtil; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -36,35 +43,35 @@ public class NettyHttpClientBuilder { private static final String EPOLL = "io.netty.channel.epoll.Epoll"; private static final String EPOLL_CHANNEL = "io.netty.channel.epoll.EpollSocketChannel"; - private static final String EPOLL_EVENT_LOOP_GROUP = "io.netty.channel.epoll.EpollEventLoopGroup"; + private static final String EPOLL_IO_HANDLER = "io.netty.channel.epoll.EpollIoHandler"; private static final boolean IS_EPOLL_AVAILABLE; private static final Class EPOLL_CHANNEL_CLASS; - private static final Class EPOLL_EVENT_LOOP_GROUP_CLASS; - private static final MethodHandle EPOLL_EVENT_LOOP_GROUP_CREATOR; + private static final Class EPOLL_IO_HANDLER_CLASS; + private static final MethodHandle EPOLL_IO_HANDLER_FACTORY_CREATOR; private static final String KQUEUE = "io.netty.channel.kqueue.KQueue"; private static final String KQUEUE_CHANNEL = "io.netty.channel.kqueue.KQueueSocketChannel"; - private static final String KQUEUE_EVENT_LOOP_GROUP = "io.netty.channel.kqueue.KQueueEventLoopGroup"; + private static final String KQUEUE_IO_HANDLER = "io.netty.channel.kqueue.KQueueIoHandler"; private static final boolean IS_KQUEUE_AVAILABLE; private static final Class KQUEUE_CHANNEL_CLASS; - private static final Class KQUEUE_EVENT_LOOP_GROUP_CLASS; - private static final MethodHandle KQUEUE_EVENT_LOOP_GROUP_CREATOR; + private static final Class KQUEUE_IO_HANDLER_CLASS; + private static final MethodHandle KQUEUE_IO_HANDLER_FACTORY_CREATOR; static { // Inspect the class path to determine is native transports are available. // If they are, this will determine runtime behaviors. boolean isEpollAvailable; Class epollChannelClass; - Class epollEventLoopGroupClass; - MethodHandle epollEventLoopGroupCreator; + Class epollIoHandlerClass; + MethodHandle epollIoHandlerFactoryCreator; try { Class epollClass = Class.forName(EPOLL); isEpollAvailable = (boolean) epollClass.getDeclaredMethod("isAvailable").invoke(null); epollChannelClass = getChannelClass(EPOLL_CHANNEL); - epollEventLoopGroupClass = Class.forName(EPOLL_EVENT_LOOP_GROUP); - epollEventLoopGroupCreator = MethodHandles.publicLookup() - .unreflectConstructor(epollEventLoopGroupClass.getDeclaredConstructor(ThreadFactory.class)); + epollIoHandlerClass = Class.forName(EPOLL_IO_HANDLER).asSubclass(IoHandler.class); + epollIoHandlerFactoryCreator + = MethodHandles.publicLookup().unreflect(epollIoHandlerClass.getDeclaredMethod("newFactory")); LOGGER.atVerbose() .addKeyValue("epollAvailable", isEpollAvailable) .log("Lookup for Epoll completed without error."); @@ -77,27 +84,27 @@ public class NettyHttpClientBuilder { LOGGER.atVerbose().log("Epoll is unavailable and won't be used."); isEpollAvailable = false; epollChannelClass = null; - epollEventLoopGroupClass = null; - epollEventLoopGroupCreator = null; + epollIoHandlerClass = null; + epollIoHandlerFactoryCreator = null; } IS_EPOLL_AVAILABLE = isEpollAvailable; EPOLL_CHANNEL_CLASS = epollChannelClass; - EPOLL_EVENT_LOOP_GROUP_CLASS = epollEventLoopGroupClass; - EPOLL_EVENT_LOOP_GROUP_CREATOR = epollEventLoopGroupCreator; + EPOLL_IO_HANDLER_CLASS = epollIoHandlerClass; + EPOLL_IO_HANDLER_FACTORY_CREATOR = epollIoHandlerFactoryCreator; boolean isKqueueAvailable; Class kqueueChannelClass; - Class kqueueEventLoopGroupClass; - MethodHandle kqueueEventLoopGroupCreator; + Class kqueueIoHandlerClass; + MethodHandle kqueueIoHandlerFactoryCreator; try { Class kqueueClass = Class.forName(KQUEUE); isKqueueAvailable = (boolean) kqueueClass.getDeclaredMethod("isAvailable").invoke(null); kqueueChannelClass = getChannelClass(KQUEUE_CHANNEL); - kqueueEventLoopGroupClass = Class.forName(KQUEUE_EVENT_LOOP_GROUP); - kqueueEventLoopGroupCreator = MethodHandles.publicLookup() - .unreflectConstructor(kqueueEventLoopGroupClass.getDeclaredConstructor(ThreadFactory.class)); + kqueueIoHandlerClass = Class.forName(KQUEUE_IO_HANDLER).asSubclass(IoHandler.class); + kqueueIoHandlerFactoryCreator + = MethodHandles.publicLookup().unreflect(kqueueIoHandlerClass.getDeclaredMethod("newFactory")); LOGGER.atVerbose() .addKeyValue("kqueueAvailable", isKqueueAvailable) .log("Lookup for KQueue completed without error."); @@ -110,14 +117,14 @@ public class NettyHttpClientBuilder { LOGGER.atVerbose().log("KQueue is unavailable and won't be used."); isKqueueAvailable = false; kqueueChannelClass = null; - kqueueEventLoopGroupClass = null; - kqueueEventLoopGroupCreator = null; + kqueueIoHandlerClass = null; + kqueueIoHandlerFactoryCreator = null; } IS_KQUEUE_AVAILABLE = isKqueueAvailable; KQUEUE_CHANNEL_CLASS = kqueueChannelClass; - KQUEUE_EVENT_LOOP_GROUP_CLASS = kqueueEventLoopGroupClass; - KQUEUE_EVENT_LOOP_GROUP_CREATOR = kqueueEventLoopGroupCreator; + KQUEUE_IO_HANDLER_CLASS = kqueueIoHandlerClass; + KQUEUE_IO_HANDLER_FACTORY_CREATOR = kqueueIoHandlerFactoryCreator; } @SuppressWarnings("unchecked") @@ -154,10 +161,10 @@ public NettyHttpClientBuilder() { * Sets the event loop group for the Netty client. *

* By default, if no {@code eventLoopGroup} is configured and no native transports are available (Epoll KQueue) - * {@link NioEventLoopGroup} will be used. + * a {@link MultiThreadIoEventLoopGroup} using {@link NioIoHandler} will be used. *

- * If native transports are available, the {@link EventLoopGroup} implementation for the native transport will be - * chosen over {@link NioEventLoopGroup}. + * If native transports are available, the {@link IoHandlerFactory} for the native transport will be + * chosen over {@link NioIoHandler}. * * @param eventLoopGroup The event loop group. * @return The updated builder. @@ -377,9 +384,9 @@ public NettyHttpClientBuilder maxPendingAcquires(int maxPendingAcquires) { */ public HttpClient build() { EventLoopGroup group = getEventLoopGroupToUse(this.eventLoopGroup, this.channelClass, IS_EPOLL_AVAILABLE, - EPOLL_EVENT_LOOP_GROUP_CREATOR, IS_KQUEUE_AVAILABLE, KQUEUE_EVENT_LOOP_GROUP_CREATOR); + EPOLL_IO_HANDLER_FACTORY_CREATOR, IS_KQUEUE_AVAILABLE, KQUEUE_IO_HANDLER_FACTORY_CREATOR); Class channelClass - = getChannelClass(this.channelClass, group.getClass(), IS_EPOLL_AVAILABLE, IS_KQUEUE_AVAILABLE); + = getChannelClass(this.channelClass, group, IS_EPOLL_AVAILABLE, IS_KQUEUE_AVAILABLE); // Leave breadcrumbs about the NettyHttpClient configuration, in case troubleshooting is needed. LoggingEvent loggingEvent = LOGGER.atVerbose() @@ -401,6 +408,13 @@ public HttpClient build() { Bootstrap bootstrap = new Bootstrap().group(group) .channel(channelClass) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) getTimeoutMillis(connectTimeout, 10_000)); + bootstrap.option(ChannelOption.ALLOCATOR, + // If io.netty.allocator.type was configured, respect the configuration. Otherwise, fallback to the default + // setting from Netty 4.1 which was the pooled allocator. In the future, this should be validated for if + // this should use the new Netty 4.2 default which is an adaptive allocator. + SystemPropertyUtil.contains("io.netty.allocator.type") + ? ByteBufAllocator.DEFAULT + : PooledByteBufAllocator.DEFAULT); // Disable auto-read as we want to control when and how data is read from the channel. bootstrap.option(ChannelOption.AUTO_READ, false); // Enable TCP keep-alive to proactively detect and clean up stale connections in the pool. This helps evict @@ -455,47 +469,52 @@ ProxyOptions getProxyOptions() { static EventLoopGroup getEventLoopGroupToUse(EventLoopGroup configuredGroup, Class configuredChannelClass, boolean isEpollAvailable, - MethodHandle epollEventLoopGroupCreator, boolean isKqueueAvailable, MethodHandle kqueueEventLoopGroupCreator) { + MethodHandle epollIoHandlerFactoryCreator, boolean isKqueueAvailable, + MethodHandle kqueueIoHandlerFactoryCreator) { if (configuredGroup != null) { return configuredGroup; } ThreadFactory threadFactory = new DefaultThreadFactory("clientcore-netty-client", true); - // Use EpollEventLoopGroup if Epoll is available and 'channelClass' wasn't configured or was configured to + // Use Epoll if available and 'channelClass' wasn't configured or was configured to // EpollSocketChannel. if (isEpollAvailable && (configuredChannelClass == null || configuredChannelClass == EPOLL_CHANNEL_CLASS)) { try { - return (EventLoopGroup) epollEventLoopGroupCreator.invoke(threadFactory); + return new MultiThreadIoEventLoopGroup(threadFactory, + (IoHandlerFactory) epollIoHandlerFactoryCreator.invoke()); } catch (Throwable ex) { - LOGGER.atVerbose().setThrowable(ex).log("Failed to create an EpollEventLoopGroup."); + LOGGER.atVerbose().setThrowable(ex).log("Failed to create an Epoll event loop group."); } } - // Use KQueueEventLoopGroup if KQueue is available and 'channelClass' wasn't configured or was configured to + // Use KQueue if available and 'channelClass' wasn't configured or was configured to // KQueueSocketChannel. if (isKqueueAvailable && (configuredChannelClass == null || configuredChannelClass == KQUEUE_CHANNEL_CLASS)) { try { - return (EventLoopGroup) kqueueEventLoopGroupCreator.invoke(threadFactory); + return new MultiThreadIoEventLoopGroup(threadFactory, + (IoHandlerFactory) kqueueIoHandlerFactoryCreator.invoke()); } catch (Throwable ex) { - LOGGER.atVerbose().setThrowable(ex).log("Failed to create a KQueueEventLoopGroup."); + LOGGER.atVerbose().setThrowable(ex).log("Failed to create a KQueue event loop group."); } } - // Fallback to NioEventLoopGroup. - return new NioEventLoopGroup(threadFactory); + // Fall back to NIO. + return new MultiThreadIoEventLoopGroup(threadFactory, NioIoHandler.newFactory()); } static Class getChannelClass(Class configuredChannelClass, - Class configuredGroupClass, boolean isEpollAvailable, boolean isKqueueAvailable) { + EventLoopGroup configuredGroup, boolean isEpollAvailable, boolean isKqueueAvailable) { if (configuredChannelClass != null) { // If the Channel class was manually set, use it. return configuredChannelClass; - } else if (isEpollAvailable && configuredGroupClass == EPOLL_EVENT_LOOP_GROUP_CLASS) { - // If Epoll is available and the EventLoopGroup is EpollEventLoopGroup, use EpollSocketChannel. + } else if (isEpollAvailable + && configuredGroup instanceof IoEventLoopGroup + && ((IoEventLoopGroup) configuredGroup).isIoType(EPOLL_IO_HANDLER_CLASS)) { return EPOLL_CHANNEL_CLASS; - } else if (isKqueueAvailable && configuredGroupClass == KQUEUE_EVENT_LOOP_GROUP_CLASS) { - // If KQueue is available and the EventLoopGroup is KQueueEventLoopGroup, use KQueueSocketChannel. + } else if (isKqueueAvailable + && configuredGroup instanceof IoEventLoopGroup + && ((IoEventLoopGroup) configuredGroup).isIoType(KQUEUE_IO_HANDLER_CLASS)) { return KQUEUE_CHANNEL_CLASS; } else { // Fallback to NioSocketChannel. diff --git a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4Utility.java b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4Utility.java index f0d7835c190f7..4bdffdc966f88 100644 --- a/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4Utility.java +++ b/sdk/clientcore/http-netty4/src/main/java/io/clientcore/http/netty4/implementation/Netty4Utility.java @@ -85,10 +85,12 @@ public final class Netty4Utility { // Non-native dependencies are required while native dependencies are optional. Without the native dependencies // the SDK will fall back to using the JDK implementations. private static final List REQUIRED_NETTY_VERSION_ARTIFACTS - = Arrays.asList("netty-buffer", "netty-codec", "netty-codec-http", "netty-codec-http2", "netty-common", - "netty-handler", "netty-handler-proxy", "netty-resolver", "netty-resolver-dns", "netty-transport"); - private static final List OPTIONAL_NETTY_VERSION_ARTIFACTS = Arrays - .asList("netty-transport-native-unix-common", "netty-transport-native-epoll", "netty-transport-native-kqueue"); + = Arrays.asList("netty-buffer", "netty-codec-base", "netty-codec-compression", "netty-codec-dns", + "netty-codec-socks", "netty-codec-http", "netty-codec-http2", "netty-common", "netty-handler", + "netty-handler-proxy", "netty-resolver", "netty-resolver-dns", "netty-transport"); + private static final List OPTIONAL_NETTY_VERSION_ARTIFACTS + = Arrays.asList("netty-transport-native-unix-common", "netty-transport-native-epoll", + "netty-transport-native-kqueue", "netty-transport-classes-epoll", "netty-transport-classes-kqueue"); private static final int TWO_FIFTY_SIX_KB = 256 * 1024; diff --git a/sdk/clientcore/http-netty4/src/main/java/module-info.java b/sdk/clientcore/http-netty4/src/main/java/module-info.java index 2ab0fc5e7223a..a9e31d53266bd 100644 --- a/sdk/clientcore/http-netty4/src/main/java/module-info.java +++ b/sdk/clientcore/http-netty4/src/main/java/module-info.java @@ -14,6 +14,7 @@ requires io.netty.common; requires io.netty.handler.proxy; requires io.netty.codec; + requires io.netty.codec.compression; requires io.netty.resolver; requires io.netty.codec.http2; requires java.sql; diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttp2HttpClientTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttp2HttpClientTests.java index dba6aa10bbeae..7a454a7650fe2 100644 --- a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttp2HttpClientTests.java +++ b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttp2HttpClientTests.java @@ -17,12 +17,21 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPOutputStream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; @Timeout(value = 3, unit = TimeUnit.MINUTES) public class NettyHttp2HttpClientTests extends HttpClientTests { @@ -93,4 +102,31 @@ public void canSendBinaryDataDebug() { assertArrayEquals(expectedBytes, response.getValue().toBytes()); } } + + @ParameterizedTest + @ValueSource(strings = { "gzip", "deflate" }) + public void decompressesResponse(String contentEncoding) throws IOException { + byte[] expected = "Compressed HTTP/2 response".getBytes(StandardCharsets.UTF_8); + ByteArrayOutputStream compressed = new ByteArrayOutputStream(); + try (OutputStream compressor = "gzip".equals(contentEncoding) + ? new GZIPOutputStream(compressed) + : new DeflaterOutputStream(compressed)) { + compressor.write(expected); + } + byte[] compressedBytes = compressed.toByteArray(); + LocalTestServer compressedServer + = new LocalTestServer(HttpProtocolVersion.HTTP_2, true, (request, response, requestBody) -> { + response.setHeader("Content-Encoding", contentEncoding); + response.setContentLength(compressedBytes.length); + response.getOutputStream().write(compressedBytes); + }); + compressedServer.start(); + try (Response response = getHttpClient() + .send(new HttpRequest().setMethod(HttpMethod.GET).setUri(compressedServer.getHttpsUri()))) { + assertEquals(200, response.getStatusCode()); + assertArrayEquals(expected, response.getValue().toBytes()); + } finally { + compressedServer.stop(); + } + } } diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttpClientBuilderTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttpClientBuilderTests.java index a417ef32f253a..ac0e01e2f2add 100644 --- a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttpClientBuilderTests.java +++ b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/NettyHttpClientBuilderTests.java @@ -13,15 +13,23 @@ import io.clientcore.core.utils.configuration.Configuration; import io.clientcore.http.netty4.implementation.NettyHttpClientLocalTestServer; import io.netty.bootstrap.BootstrapConfig; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; -import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.IoEventLoopGroup; +import io.netty.channel.IoHandler; +import io.netty.channel.IoHandlerFactory; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.epoll.EpollIoHandler; import io.netty.channel.epoll.EpollSocketChannel; -import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueIoHandler; import io.netty.channel.kqueue.KQueueSocketChannel; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.resolver.DefaultAddressResolverGroup; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.condition.EnabledOnOs; @@ -38,7 +46,6 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Stream; @@ -50,6 +57,13 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; /** * Tests {@link NettyHttpClientBuilder}. @@ -196,6 +210,7 @@ private static Stream buildWithEnvConfigurationProxySupplier() { arguments.add( Configuration.from(new TestConfigurationSource().put(Configuration.HTTP_PROXY, "http://localhost:12345") + .put(Configuration.HTTPS_PROXY, "") .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true"))); /* @@ -207,6 +222,7 @@ private static Stream buildWithEnvConfigurationProxySupplier() { arguments.add( Configuration.from(new TestConfigurationSource().put(Configuration.HTTP_PROXY, "http://1:1@localhost:12345") + .put(Configuration.HTTPS_PROXY, "") .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true"))); /* @@ -222,6 +238,7 @@ private static Stream buildWithEnvConfigurationProxySupplier() { .from(baseJavaProxyConfigurationSupplier.get().put(JAVA_NON_PROXY_HOSTS, rawJavaNonProxyHosts))); arguments.add( Configuration.from(new TestConfigurationSource().put(Configuration.HTTP_PROXY, "http://localhost:12345") + .put(Configuration.HTTPS_PROXY, "") .put(Configuration.NO_PROXY, rawEnvNonProxyHosts) .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true"))); @@ -234,6 +251,7 @@ private static Stream buildWithEnvConfigurationProxySupplier() { .put(JAVA_HTTP_PROXY_PASSWORD, "1"))); arguments.add( Configuration.from(new TestConfigurationSource().put(Configuration.HTTP_PROXY, "http://1:1@localhost:12345") + .put(Configuration.HTTPS_PROXY, "") .put(Configuration.NO_PROXY, rawEnvNonProxyHosts) .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true"))); @@ -286,12 +304,16 @@ private static Stream buildWithExplicitConfigurationProxySupplier @Test public void buildEventLoopClient() { String expectedThreadName = "testEventLoop"; - NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup(1, (Runnable r) -> new Thread(r, expectedThreadName)); - - NettyHttpClient nettyClient - = (NettyHttpClient) new NettyHttpClientBuilder().eventLoopGroup(eventLoopGroup).build(); - - assertSame(eventLoopGroup, nettyClient.getBootstrap().config().group()); + EventLoopGroup eventLoopGroup = new MultiThreadIoEventLoopGroup(1, + (Runnable r) -> new Thread(r, expectedThreadName), NioIoHandler.newFactory()); + + try { + NettyHttpClient nettyClient + = (NettyHttpClient) new NettyHttpClientBuilder().eventLoopGroup(eventLoopGroup).build(); + assertSame(eventLoopGroup, nettyClient.getBootstrap().config().group()); + } finally { + eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS).syncUninterruptibly(); + } } @ParameterizedTest @@ -307,13 +329,27 @@ private static Stream getTimeoutMillisSupplier() { Arguments.of(Duration.ofNanos(1), TimeUnit.MILLISECONDS.toMillis(1))); } + @Test + public void preservesAllocatorAndResolverDefaults() { + NettyHttpClient client = (NettyHttpClient) new NettyHttpClientBuilder().build(); + try { + ByteBufAllocator expectedAllocator = System.getProperty("io.netty.allocator.type") == null + ? PooledByteBufAllocator.DEFAULT + : ByteBufAllocator.DEFAULT; + assertSame(expectedAllocator, client.getBootstrap().config().options().get(ChannelOption.ALLOCATOR)); + assertSame(DefaultAddressResolverGroup.INSTANCE, client.getBootstrap().config().resolver()); + } finally { + client.getBootstrap().config().group().shutdownGracefully(0, 5, TimeUnit.SECONDS).syncUninterruptibly(); + } + } + @Test @EnabledOnOs(OS.WINDOWS) public void windowsUseNioByDefault() { NettyHttpClient nettyHttpClient = (NettyHttpClient) new NettyHttpClientBuilder().build(); BootstrapConfig config = nettyHttpClient.getBootstrap().config(); - assertInstanceOf(NioEventLoopGroup.class, config.group()); + assertTrue(assertInstanceOf(IoEventLoopGroup.class, config.group()).isIoType(NioIoHandler.class)); assertInstanceOf(NioSocketChannel.class, config.channelFactory().newChannel()); } @@ -323,7 +359,7 @@ public void macUsesKQueueByDefault() { NettyHttpClient nettyHttpClient = (NettyHttpClient) new NettyHttpClientBuilder().build(); BootstrapConfig config = nettyHttpClient.getBootstrap().config(); - assertInstanceOf(KQueueEventLoopGroup.class, config.group()); + assertTrue(assertInstanceOf(IoEventLoopGroup.class, config.group()).isIoType(KQueueIoHandler.class)); assertInstanceOf(KQueueSocketChannel.class, config.channelFactory().newChannel()); } @@ -332,11 +368,11 @@ public void macUsesKQueueByDefault() { public void macUsesNioIfConfigured() { NettyHttpClient nettyHttpClient = (NettyHttpClient) new NettyHttpClientBuilder().channelClass(NioSocketChannel.class) - .eventLoopGroup(new NioEventLoopGroup()) + .eventLoopGroup(new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory())) .build(); BootstrapConfig config = nettyHttpClient.getBootstrap().config(); - assertInstanceOf(NioEventLoopGroup.class, config.group()); + assertTrue(assertInstanceOf(IoEventLoopGroup.class, config.group()).isIoType(NioIoHandler.class)); assertInstanceOf(NioSocketChannel.class, config.channelFactory().newChannel()); } @@ -346,7 +382,7 @@ public void linuxUsesEpollByDefault() { NettyHttpClient nettyHttpClient = (NettyHttpClient) new NettyHttpClientBuilder().build(); BootstrapConfig config = nettyHttpClient.getBootstrap().config(); - assertInstanceOf(EpollEventLoopGroup.class, config.group()); + assertTrue(assertInstanceOf(IoEventLoopGroup.class, config.group()).isIoType(EpollIoHandler.class)); assertInstanceOf(EpollSocketChannel.class, config.channelFactory().newChannel()); } @@ -355,24 +391,61 @@ public void linuxUsesEpollByDefault() { public void linuxUsesNioIfConfigured() { NettyHttpClient nettyHttpClient = (NettyHttpClient) new NettyHttpClientBuilder().channelClass(NioSocketChannel.class) - .eventLoopGroup(new NioEventLoopGroup()) + .eventLoopGroup(new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory())) .build(); BootstrapConfig config = nettyHttpClient.getBootstrap().config(); - assertInstanceOf(NioEventLoopGroup.class, config.group()); + assertTrue(assertInstanceOf(IoEventLoopGroup.class, config.group()).isIoType(NioIoHandler.class)); assertInstanceOf(NioSocketChannel.class, config.channelFactory().newChannel()); } @ParameterizedTest @MethodSource("getEventLoopGroupToUseSupplier") - public void getEventLoopGroupToUse(Class expected, EventLoopGroup configuredGroup, - Class configuredChannelClass, boolean isEpollAvailable, - MethodHandle epollEventLoopGroupCreator, boolean isKqueueAvailable, MethodHandle kqueueEventLoopGroupCreator) { - EventLoopGroup eventLoopGroup - = NettyHttpClientBuilder.getEventLoopGroupToUse(configuredGroup, configuredChannelClass, isEpollAvailable, - epollEventLoopGroupCreator, isKqueueAvailable, kqueueEventLoopGroupCreator); - - assertInstanceOf(expected, eventLoopGroup); + public void getEventLoopGroupToUse(String expectedTransport, boolean configureGroup, + Class configuredChannelClass, boolean isEpollAvailable, boolean epollFails, + boolean isKqueueAvailable, boolean kqueueFails) { + IoHandlerFactory epollFactory = mockIoHandlerFactory(); + IoHandlerFactory kqueueFactory = mockIoHandlerFactory(); + MethodHandle exceptionCreator + = MethodHandles.throwException(IoHandlerFactory.class, IllegalStateException.class) + .bindTo(new IllegalStateException("Native transport unavailable")); + MethodHandle epollCreator + = epollFails ? exceptionCreator : MethodHandles.constant(IoHandlerFactory.class, epollFactory); + MethodHandle kqueueCreator + = kqueueFails ? exceptionCreator : MethodHandles.constant(IoHandlerFactory.class, kqueueFactory); + EventLoopGroup configuredGroup + = configureGroup ? new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()) : null; + EventLoopGroup eventLoopGroup = NettyHttpClientBuilder.getEventLoopGroupToUse(configuredGroup, + configuredChannelClass, isEpollAvailable, epollCreator, isKqueueAvailable, kqueueCreator); + + try { + assertInstanceOf(MultiThreadIoEventLoopGroup.class, eventLoopGroup); + if (configuredGroup != null) { + assertSame(configuredGroup, eventLoopGroup); + } + if ("epoll".equals(expectedTransport)) { + verify(epollFactory, atLeastOnce()).newHandler(any()); + verifyNoInteractions(kqueueFactory); + } else if ("kqueue".equals(expectedTransport)) { + verify(kqueueFactory, atLeastOnce()).newHandler(any()); + verifyNoInteractions(epollFactory); + } else { + verifyNoInteractions(epollFactory, kqueueFactory); + } + assertTrue(assertInstanceOf(IoEventLoopGroup.class, eventLoopGroup).isIoType(NioIoHandler.class)); + if (configuredGroup == null) { + assertTrue(eventLoopGroup.next() + .submit(() -> Thread.currentThread().isDaemon()) + .syncUninterruptibly() + .getNow()); + assertTrue(eventLoopGroup.next() + .submit(() -> Thread.currentThread().getName().startsWith("clientcore-netty-client")) + .syncUninterruptibly() + .getNow()); + } + } finally { + eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS).syncUninterruptibly(); + } } @Test @@ -404,128 +477,60 @@ public void testMaximumHttpVersion() throws NoSuchFieldException, IllegalAccessE assertEquals(HttpProtocolVersion.HTTP_2, httpVersionField.get(clientv2)); } - private static Stream getEventLoopGroupToUseSupplier() throws ReflectiveOperationException { - // Doesn't matter what this is calling, just needs to throw an exception. - // This will as it doesn't accept the arguments that it will be called with. - MethodHandle exceptionCreator - = MethodHandles.publicLookup().unreflectConstructor(NioEventLoopGroup.class.getDeclaredConstructor()); - - // NOTE: This test doesn't use EpollEventLoopGroup or KQueueEventLoopGroup directly, but rather uses different - // EventLoopGroup classes as the creation of those requires native libraries to be loaded. - // This is a workaround to avoid loading the native libraries in the test, as not all OSes can support the - // native transports. - MethodHandle epollCreator = MethodHandles.publicLookup() - .unreflectConstructor(MockEpollEventLoopGroup.class.getDeclaredConstructor(ThreadFactory.class)); - MethodHandle kqueueCreator = MethodHandles.publicLookup() - .unreflectConstructor(MockKQueueEventLoopGroup.class.getDeclaredConstructor(ThreadFactory.class)); - - // EventLoopGroup is configured, use it. - Arguments configuredGroup - = Arguments.of(NioEventLoopGroup.class, new NioEventLoopGroup(), null, false, null, false, null); - - // Epoll is available and nothing is configured, use EpollEventLoopGroup. - Arguments epollGroup = Arguments.of(MockEpollEventLoopGroup.class, null, null, true, epollCreator, false, null); - - // Epoll is available and EpollSocketChannel is configured, use EpollEventLoopGroup. - Arguments epollChannelGroup = Arguments.of(MockEpollEventLoopGroup.class, null, EpollSocketChannel.class, true, - epollCreator, false, null); - - // Epoll is available but throws an exception, use NioEventLoopGroup. - Arguments epollExceptionGroup - = Arguments.of(NioEventLoopGroup.class, null, null, true, exceptionCreator, false, null); - - // KQueue is available and nothing is configured, use KQueueEventLoopGroup. - Arguments kqueueGroup - = Arguments.of(MockKQueueEventLoopGroup.class, null, null, false, null, true, kqueueCreator); - - // KQueue is available and KQueueSocketChannel is configured, use KQueueEventLoopGroup. - Arguments kqueueChannelGroup = Arguments.of(MockKQueueEventLoopGroup.class, null, KQueueSocketChannel.class, - false, null, true, kqueueCreator); - - // KQueue is available but throws an exception, use NioEventLoopGroup. - Arguments kqueueExceptionGroup - = Arguments.of(NioEventLoopGroup.class, null, null, false, null, true, exceptionCreator); - - // Both Epoll and KQueue are available, use EpollEventLoopGroup. - Arguments epollAndKqueueGroup - = Arguments.of(MockEpollEventLoopGroup.class, null, null, true, epollCreator, true, kqueueCreator); - - // Both Epoll and KQueue are available but channel class is set to KQueueSocketChannel, use - // KQueueEventLoopGroup. - Arguments epollAndKqueueChannelGroup = Arguments.of(MockKQueueEventLoopGroup.class, null, - KQueueSocketChannel.class, true, epollCreator, true, kqueueCreator); - - // Both Epoll and KQueue are available but throws an exception, use NioEventLoopGroup. - Arguments epollAndKqueueExceptionGroup - = Arguments.of(NioEventLoopGroup.class, null, null, true, exceptionCreator, true, exceptionCreator); - - // Both Epoll and KQueue are available but channel class is set to EpollSocketChannel, use - // EpollEventLoopGroup. - Arguments epollAndKqueueChannelExceptionGroup = Arguments.of(MockEpollEventLoopGroup.class, null, - EpollSocketChannel.class, true, epollCreator, true, kqueueCreator); - - // Both Epoll and KQueue are available but channel class is set to NioSocketChannel, use - // NioEventLoopGroup. - Arguments epollAndKqueueChannelNioGroup = Arguments.of(NioEventLoopGroup.class, null, NioSocketChannel.class, - true, epollCreator, true, kqueueCreator); - - return Stream.of(configuredGroup, epollGroup, epollChannelGroup, epollExceptionGroup, kqueueGroup, - kqueueChannelGroup, kqueueExceptionGroup, epollAndKqueueGroup, epollAndKqueueChannelGroup, - epollAndKqueueExceptionGroup, epollAndKqueueChannelExceptionGroup, epollAndKqueueChannelNioGroup); + private static Stream getEventLoopGroupToUseSupplier() { + return Stream.of(Arguments.of("nio", true, null, true, true, true, true), + Arguments.of("epoll", false, null, true, false, false, false), + Arguments.of("epoll", false, EpollSocketChannel.class, true, false, false, false), + Arguments.of("nio", false, null, true, true, false, false), + Arguments.of("kqueue", false, null, false, false, true, false), + Arguments.of("kqueue", false, KQueueSocketChannel.class, false, false, true, false), + Arguments.of("nio", false, null, false, false, true, true), + Arguments.of("epoll", false, null, true, false, true, false), + Arguments.of("kqueue", false, KQueueSocketChannel.class, true, false, true, false), + Arguments.of("kqueue", false, null, true, true, true, false), + Arguments.of("nio", false, null, true, true, true, true), + Arguments.of("epoll", false, EpollSocketChannel.class, true, false, true, false), + Arguments.of("nio", false, NioSocketChannel.class, true, true, true, true), + Arguments.of("nio", false, null, false, false, false, false)); } - public static final class MockEpollEventLoopGroup extends NioEventLoopGroup { - public MockEpollEventLoopGroup(ThreadFactory threadFactory) { - super(threadFactory); - } - } - - public static final class MockKQueueEventLoopGroup extends NioEventLoopGroup { - public MockKQueueEventLoopGroup(ThreadFactory threadFactory) { - super(threadFactory); - } + private static IoHandlerFactory mockIoHandlerFactory() { + // Delegate to NIO so factory selection can be tested without loading native libraries. + IoHandlerFactory delegate = NioIoHandler.newFactory(); + IoHandlerFactory factory = mock(IoHandlerFactory.class); + when(factory.newHandler(any())).thenAnswer(invocation -> delegate.newHandler(invocation.getArgument(0))); + return factory; } @ParameterizedTest @MethodSource("getChannelClassSupplier") public void getChannelClass(Class expected, Class configuredChannelClass, - Class congiguredGroupClass, boolean isEpollAvailable, boolean isKqueueAvailable) { + EventLoopGroup configuredGroup, boolean isEpollAvailable, boolean isKqueueAvailable) { Class channelClass = NettyHttpClientBuilder.getChannelClass(configuredChannelClass, - congiguredGroupClass, isEpollAvailable, isKqueueAvailable); + configuredGroup, isEpollAvailable, isKqueueAvailable); assertEquals(expected, channelClass); } private static Stream getChannelClassSupplier() { - // Channel class is configured, use it. - Arguments configuredChannel = Arguments.of(NioSocketChannel.class, NioSocketChannel.class, null, false, false); - - // Epoll is available and EventLoopGroup is EpollEventLoopGroup, use EpollSocketChannel. - Arguments epollChannel = Arguments.of(EpollSocketChannel.class, null, EpollEventLoopGroup.class, true, false); - - // KQueue is available and EventLoopGroup is KQueueEventLoopGroup, use KQueueSocketChannel. - Arguments kqueueChannel - = Arguments.of(KQueueSocketChannel.class, null, KQueueEventLoopGroup.class, false, true); - - // Epoll is available and EventLoopGroup is NioEventLoopGroup, use NioSocketChannel. - Arguments epollNioChannel = Arguments.of(NioSocketChannel.class, null, NioEventLoopGroup.class, true, false); - - // KQueue is available and EventLoopGroup is NioEventLoopGroup, use NioSocketChannel. - Arguments kqueueNioChannel = Arguments.of(NioSocketChannel.class, null, NioEventLoopGroup.class, false, true); - - // Both Epoll and KQueue are available and EventLoopGroup is NioEventLoopGroup, use NioSocketChannel. - Arguments epollAndKqueueNioChannel - = Arguments.of(NioSocketChannel.class, null, NioEventLoopGroup.class, true, true); - - // Both Epoll and KQueue are available and EventLoopGroup is EpollEventLoopGroup, use EpollSocketChannel. - Arguments epollAndKqueueEpollChannel - = Arguments.of(EpollSocketChannel.class, null, EpollEventLoopGroup.class, true, true); - - // Both Epoll and KQueue are available and EventLoopGroup is KQueueEventLoopGroup, use KQueueSocketChannel. - Arguments epollAndKqueueKqueueChannel - = Arguments.of(KQueueSocketChannel.class, null, KQueueEventLoopGroup.class, true, true); + IoEventLoopGroup nioGroup = mockIoEventLoopGroup(NioIoHandler.class); + IoEventLoopGroup epollGroup = mockIoEventLoopGroup(EpollIoHandler.class); + IoEventLoopGroup kqueueGroup = mockIoEventLoopGroup(KQueueIoHandler.class); + + return Stream.of(Arguments.of(NioSocketChannel.class, NioSocketChannel.class, epollGroup, true, true), + Arguments.of(EpollSocketChannel.class, null, epollGroup, true, false), + Arguments.of(KQueueSocketChannel.class, null, kqueueGroup, false, true), + Arguments.of(NioSocketChannel.class, null, nioGroup, true, false), + Arguments.of(NioSocketChannel.class, null, nioGroup, false, true), + Arguments.of(NioSocketChannel.class, null, nioGroup, true, true), + Arguments.of(EpollSocketChannel.class, null, epollGroup, true, true), + Arguments.of(KQueueSocketChannel.class, null, kqueueGroup, true, true), + Arguments.of(NioSocketChannel.class, null, mock(EventLoopGroup.class), true, true)); + } - return Stream.of(configuredChannel, epollChannel, kqueueChannel, epollNioChannel, kqueueNioChannel, - epollAndKqueueNioChannel, epollAndKqueueEpollChannel, epollAndKqueueKqueueChannel); + private static IoEventLoopGroup mockIoEventLoopGroup(Class ioHandlerClass) { + IoEventLoopGroup group = mock(IoEventLoopGroup.class); + when(group.isIoType(ioHandlerClass)).thenReturn(true); + return group; } } diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4ConnectionPoolTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4ConnectionPoolTests.java index ba84eb3e90351..25f02cd4d45f6 100644 --- a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4ConnectionPoolTests.java +++ b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4ConnectionPoolTests.java @@ -10,7 +10,8 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.util.concurrent.Future; import org.junit.jupiter.api.AfterAll; @@ -63,7 +64,7 @@ public class Netty4ConnectionPoolTests { public static void startTestServerAndEventLoopGroup() { server = NettyHttpClientLocalTestServer.getServer(); server.start(); - eventLoopGroup = new NioEventLoopGroup(2); + eventLoopGroup = new MultiThreadIoEventLoopGroup(2, NioIoHandler.newFactory()); bootstrap = new Bootstrap().group(eventLoopGroup).channel(NioSocketChannel.class); bootstrap.option(ChannelOption.AUTO_READ, false); SocketAddress socketAddress = new InetSocketAddress("localhost", server.getPort()); diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4Http11ChannelBinaryDataTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4Http11ChannelBinaryDataTests.java index 02ccf7538c9ba..aa9a0ad8cf2dc 100644 --- a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4Http11ChannelBinaryDataTests.java +++ b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4Http11ChannelBinaryDataTests.java @@ -50,6 +50,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -260,6 +261,7 @@ public void closeAfterDrainingDisconnectsChannel() { TestMockChannel realChannel = new TestMockChannel(); new DefaultEventLoop().register(realChannel); Channel spiedChannel = spy(realChannel); + doReturn(realChannel.pipeline()).when(spiedChannel).pipeline(); Runnable cleanupTask = () -> { spiedChannel.disconnect(); spiedChannel.close(); diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4PipelineCleanupHandlerTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4PipelineCleanupHandlerTests.java index afd24f2ab29af..6df2d35cffac8 100644 --- a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4PipelineCleanupHandlerTests.java +++ b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4PipelineCleanupHandlerTests.java @@ -293,6 +293,11 @@ public int getCloseCallCount() { } private static class MockEventLoop extends DefaultEventLoop { + @Override + public boolean inEventLoop(Thread thread) { + return true; + } + @Override public void execute(Runnable task) { if (task == null) { diff --git a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4UtilityTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4UtilityTests.java index 468f57acffd0f..ffa52f66e01c0 100644 --- a/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4UtilityTests.java +++ b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4UtilityTests.java @@ -2,15 +2,22 @@ // Licensed under the MIT License. package io.clientcore.http.netty4.implementation; +import io.clientcore.core.http.client.HttpProtocolVersion; import io.clientcore.core.http.models.HttpHeader; import io.clientcore.core.http.models.HttpHeaderName; import io.clientcore.core.http.models.HttpHeaders; import io.clientcore.core.utils.CoreUtils; +import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslProvider; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import javax.net.ssl.SSLException; import java.util.Arrays; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -22,11 +29,32 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertLinesMatch; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @Timeout(value = 3, unit = TimeUnit.MINUTES) public class Netty4UtilityTests { + @ParameterizedTest + @EnumSource(HttpProtocolVersion.class) + public void sslContextEnablesHostnameVerification(HttpProtocolVersion protocolVersion) throws SSLException { + SslContext context + = Netty4Utility.buildSslContext(protocolVersion, builder -> builder.sslProvider(SslProvider.JDK)); + assertEquals("HTTPS", + context.newEngine(UnpooledByteBufAllocator.DEFAULT, "localhost", 443) + .getSSLParameters() + .getEndpointIdentificationAlgorithm()); + } + + @Test + public void sslContextHonorsHostnameVerificationModifier() throws SSLException { + SslContext context = Netty4Utility.buildSslContext(HttpProtocolVersion.HTTP_1_1, + builder -> builder.sslProvider(SslProvider.JDK).endpointIdentificationAlgorithm(null)); + assertNull(context.newEngine(UnpooledByteBufAllocator.DEFAULT, "localhost", 443) + .getSSLParameters() + .getEndpointIdentificationAlgorithm()); + } + @Test public void validateNettyVersionsWithWhatThePomSpecifies() { Map pomVersions = CoreUtils.getProperties(PROPERTIES_FILE_NAME); diff --git a/sdk/clientcore/http-okhttp3/src/test/java/io/clientcore/http/okhttp3/OkHttpHttpClientBuilderTests.java b/sdk/clientcore/http-okhttp3/src/test/java/io/clientcore/http/okhttp3/OkHttpHttpClientBuilderTests.java index a68b162b07d6f..9498ba6526916 100644 --- a/sdk/clientcore/http-okhttp3/src/test/java/io/clientcore/http/okhttp3/OkHttpHttpClientBuilderTests.java +++ b/sdk/clientcore/http-okhttp3/src/test/java/io/clientcore/http/okhttp3/OkHttpHttpClientBuilderTests.java @@ -505,6 +505,7 @@ private static Stream buildWithEnvConfigurationProxySupplier() { Configuration simpleEnvProxy = Configuration.from(new TestConfigurationSource().put(Configuration.HTTP_PROXY, "http://localhost:12345") + .put(Configuration.HTTPS_PROXY, "") .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true")); arguments.add(Arguments.of(true, simpleEnvProxy, defaultUri)); @@ -520,6 +521,7 @@ private static Stream buildWithEnvConfigurationProxySupplier() { Configuration envProxyWithAuthentication = Configuration.from(EMPTY_SOURCE, EMPTY_SOURCE, new TestConfigurationSource().put(Configuration.HTTP_PROXY, "http://1:1@localhost:12345") + .put(Configuration.HTTPS_PROXY, "") .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true")); arguments.add(Arguments.of(true, envProxyWithAuthentication, defaultUri)); @@ -544,6 +546,7 @@ private static Stream buildWithEnvConfigurationProxySupplier() { = () -> baseJavaProxyConfigurationSupplier.get().put(JAVA_NON_PROXY_HOSTS, rawJavaNonProxyHosts); Supplier envNonProxyHostsSupplier = () -> new TestConfigurationSource().put(Configuration.HTTP_PROXY, "http://localhost:12345") + .put(Configuration.HTTPS_PROXY, "") .put(Configuration.NO_PROXY, rawEnvNonProxyHosts) .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true"); List> nonProxyHostsSuppliers @@ -568,6 +571,7 @@ private static Stream buildWithEnvConfigurationProxySupplier() { = () -> javaNonProxyHostsSupplier.get().put(JAVA_HTTP_PROXY_USER, "1").put(JAVA_HTTP_PROXY_PASSWORD, "1"); Supplier authenticatedEnvNonProxyHostsSupplier = () -> new TestConfigurationSource().put(Configuration.HTTP_PROXY, "http://1:1@localhost:12345") + .put(Configuration.HTTPS_PROXY, "") .put(Configuration.NO_PROXY, rawEnvNonProxyHosts) .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true"); List> authenticatedNonProxyHostsSuppliers diff --git a/sdk/communication/azure-communication-common/pom.xml b/sdk/communication/azure-communication-common/pom.xml index da9da743a7f2d..f52555fe1afdd 100644 --- a/sdk/communication/azure-communication-common/pom.xml +++ b/sdk/communication/azure-communication-common/pom.xml @@ -80,12 +80,6 @@ 5.13.4 test - - io.projectreactor - reactor-test - 3.7.19 - test - com.azure azure-core-test diff --git a/sdk/communication/azure-communication-jobrouter/pom.xml b/sdk/communication/azure-communication-jobrouter/pom.xml index 835fd5291fe23..810722db9d86c 100644 --- a/sdk/communication/azure-communication-jobrouter/pom.xml +++ b/sdk/communication/azure-communication-jobrouter/pom.xml @@ -100,20 +100,6 @@ - - org.apache.maven.plugins - maven-enforcer-plugin - 3.6.3 - - - - - com.azure:* - - - - - diff --git a/sdk/confidentialledger/azure-security-confidentialledger/pom.xml b/sdk/confidentialledger/azure-security-confidentialledger/pom.xml index 108c068c76af0..55004dc857428 100644 --- a/sdk/confidentialledger/azure-security-confidentialledger/pom.xml +++ b/sdk/confidentialledger/azure-security-confidentialledger/pom.xml @@ -39,6 +39,9 @@ --add-exports com.azure.http.netty/com.azure.core.http.netty=com.azure.security.confidentialledger --add-reads com.azure.security.confidentialledger=com.azure.http.netty + --add-reads com.azure.security.confidentialledger=io.netty.handler + --add-reads com.azure.security.confidentialledger=reactor.netty.core + --add-reads com.azure.security.confidentialledger=reactor.netty.http false diff --git a/sdk/core/azure-core-experimental/pom.xml b/sdk/core/azure-core-experimental/pom.xml index a47a0a085b764..667da00c56592 100644 --- a/sdk/core/azure-core-experimental/pom.xml +++ b/sdk/core/azure-core-experimental/pom.xml @@ -100,7 +100,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/core/azure-core-http-jdk-httpclient/pom.xml b/sdk/core/azure-core-http-jdk-httpclient/pom.xml index 1315ec0a5e854..5dcd3b7ae31d8 100644 --- a/sdk/core/azure-core-http-jdk-httpclient/pom.xml +++ b/sdk/core/azure-core-http-jdk-httpclient/pom.xml @@ -89,7 +89,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/core/azure-core-http-jdk-httpclient/src/test/java/com/azure/core/http/jdk/httpclient/JdkHttpClientBuilderTests.java b/sdk/core/azure-core-http-jdk-httpclient/src/test/java/com/azure/core/http/jdk/httpclient/JdkHttpClientBuilderTests.java index a13e7b4e45e6e..93a85fea1b911 100644 --- a/sdk/core/azure-core-http-jdk-httpclient/src/test/java/com/azure/core/http/jdk/httpclient/JdkHttpClientBuilderTests.java +++ b/sdk/core/azure-core-http-jdk-httpclient/src/test/java/com/azure/core/http/jdk/httpclient/JdkHttpClientBuilderTests.java @@ -147,11 +147,10 @@ public void buildWithHttpProxy() { @Test public void buildWithHttpProxyFromEnvConfiguration() { - Configuration configuration = new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, - new TestConfigurationSource() - .put(Configuration.PROPERTY_HTTP_PROXY, - "http://" + PROXY_USER_INFO + "localhost:" + PROXY_SERVER_HTTP_PORT) - .put("java.net.useSystemProxies", "true")).build(); + Configuration configuration = new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, new TestConfigurationSource() + .put(Configuration.PROPERTY_HTTP_PROXY, "http://" + PROXY_USER_INFO + "localhost:" + PROXY_SERVER_HTTP_PORT) + .put(Configuration.PROPERTY_HTTPS_PROXY, "") + .put("java.net.useSystemProxies", "true")).build(); configurationProxyTest(configuration); } @@ -193,6 +192,7 @@ private static Stream buildWithExplicitConfigurationProxySupplier() { final Configuration envConfiguration = new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, new TestConfigurationSource().put(Configuration.PROPERTY_HTTP_PROXY, "http://localhost:8888") + .put(Configuration.PROPERTY_HTTPS_PROXY, "") .put(Configuration.PROPERTY_NO_PROXY, "localhost")).build(); arguments.add(Arguments.of(envConfiguration)); diff --git a/sdk/core/azure-core-http-netty/CHANGELOG.md b/sdk/core/azure-core-http-netty/CHANGELOG.md index 147e24463451f..db11cd9233df7 100644 --- a/sdk/core/azure-core-http-netty/CHANGELOG.md +++ b/sdk/core/azure-core-http-netty/CHANGELOG.md @@ -8,8 +8,16 @@ ### Bugs Fixed +- Preserved asynchronous file downloads when Netty uses scoped direct buffers on newer JDKs. +- Declared the Netty transport dependency directly to prevent older transitive transports from being selected. + ### Other Changes +#### Dependency Updates + +- Migrated Netty dependencies from `4.1.137.Final` to `4.2.19.Final`. +- Upgraded Reactor Netty from `1.2.18` to `1.3.7` and Reactor from `3.7.19` to `3.8.7`. + ## 1.16.8 (2026-09-29) ### Other Changes diff --git a/sdk/core/azure-core-http-netty/README.md b/sdk/core/azure-core-http-netty/README.md index bb7dcfcce92ec..0489ce5573be8 100644 --- a/sdk/core/azure-core-http-netty/README.md +++ b/sdk/core/azure-core-http-netty/README.md @@ -55,6 +55,47 @@ add the direct dependency to your project as follows. ## Key concepts +### Netty 4.2 compatibility + +Starting with `1.17.0-beta.1`, this library uses Netty `4.2.19.Final`, Reactor Netty `1.3.7`, and Reactor `3.8.7`. +Netty 4.1 and 4.2 must not be mixed on the classpath. If your application or framework manages these dependencies, +align the entire Netty and Reactor stack when upgrading. Java 8 remains supported. + +Netty 4.2 enables TLS hostname verification by default and changes its default buffer allocator to the adaptive allocator. +This library retains pooled allocation unless the application explicitly configures an allocator. +See the [Netty 4.2 migration guide](https://netty.io/wiki/netty-4.2-migration-guide.html#recommended-upgrade-process) +for application-level migration guidance. + +### Native transports on the module path + +On JDK 11 or later, Netty 4.2 packages the epoll/kqueue classes and their platform-specific JNI libraries +as **separate explicit Java modules**. Having the JARs on the module path does not resolve either module +automatically. A named-module application that wants native transport must include the matching +`netty-transport-native-epoll` or `netty-transport-native-kqueue` classifier JAR (which brings in its +`netty-transport-classes-*` JAR) and add **both** modules as launch roots: + +| Platform and classifier | Launch option | +| --- | --- | +| Linux x86_64 (`netty-transport-native-epoll:linux-x86_64`) | `--add-modules=io.netty.transport.classes.epoll,io.netty.transport.epoll.linux.x86_64` | +| macOS x86_64 (`netty-transport-native-kqueue:osx-x86_64`) | `--add-modules=io.netty.transport.classes.kqueue,io.netty.transport.kqueue.osx.x86_64` | + +For example, with the Linux classifier JAR and its dependencies on the module path: + +```text +java --module-path "path-to-application-and-dependency-JARs" --add-modules=io.netty.transport.classes.epoll,io.netty.transport.epoll.linux.x86_64 -m my.app/com.example.Main +``` + +This library declares the x86_64 classifier dependencies, but applications using another architecture +must supply the corresponding native classifier and resolve its module instead. Check that JAR's +module name with `jar --describe-module --file --release 11`; see +[Netty's native transport guide](https://netty.io/wiki/native-transports.html#using-the-native-transports) +for classifier details. Resolving only the classes +leaves the bundled JNI library invisible to Netty; `requires static` in a library descriptor does not +resolve either optional module at run time. If native transport is unavailable, Reactor Netty uses +JDK NIO. These launch options do not apply to Java 8 or to applications using the classpath rather +than the module path, and resolving the modules alone does not guarantee native transport is usable +on the current OS. + ## Examples The following sections provide several code snippets covering some of the most common client configuration scenarios. @@ -220,5 +261,3 @@ For details on contributing to this repository, see the [contributing guide](htt [logging]: https://learn.microsoft.com/azure/developer/java/sdk/logging-overview [jdk_link]: https://learn.microsoft.com/java/azure/jdk/?view=azure-java-stable [java8_client_compatibility]: https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details?tabs=root-and-subordinate-cas-list#client-compatibility-for-public-pkis - - diff --git a/sdk/core/azure-core-http-netty/cgmanifest.json b/sdk/core/azure-core-http-netty/cgmanifest.json index dd6d6dd04339b..0f3d3948a2dd0 100644 --- a/sdk/core/azure-core-http-netty/cgmanifest.json +++ b/sdk/core/azure-core-http-netty/cgmanifest.json @@ -9,7 +9,7 @@ "maven": { "groupId": "io.netty", "artifactId": "netty-handler-proxy", - "version": "4.1.110.Final" + "version": "4.2.19.Final" } } } diff --git a/sdk/core/azure-core-http-netty/pom.xml b/sdk/core/azure-core-http-netty/pom.xml index 5e71c171f2f08..1b4a40118e828 100644 --- a/sdk/core/azure-core-http-netty/pom.xml +++ b/sdk/core/azure-core-http-netty/pom.xml @@ -64,7 +64,7 @@ false false - 4.1.138.Final + 4.2.19.Final 2.0.84.Final @@ -85,49 +85,70 @@ io.netty netty-handler - 4.1.138.Final + 4.2.19.Final io.netty netty-handler-proxy - 4.1.138.Final + 4.2.19.Final io.netty netty-buffer - 4.1.138.Final + 4.2.19.Final io.netty - netty-codec - 4.1.138.Final + netty-transport + 4.2.19.Final + + + io.netty + netty-codec-base + 4.2.19.Final io.netty netty-codec-http - 4.1.138.Final + 4.2.19.Final io.netty netty-codec-http2 - 4.1.138.Final + 4.2.19.Final + + + io.netty + netty-codec-http3 + 4.2.19.Final + + + io.netty + netty-resolver-dns + 4.2.19.Final + + + io.netty + netty-resolver-dns-native-macos + 4.2.19.Final + osx-x86_64 io.netty netty-transport-native-unix-common - 4.1.138.Final + 4.2.19.Final io.netty netty-transport-native-epoll - 4.1.138.Final + 4.2.19.Final linux-x86_64 io.netty netty-transport-native-kqueue - 4.1.138.Final + 4.2.19.Final osx-x86_64 @@ -141,13 +162,13 @@ io.projectreactor.netty reactor-netty-http - 1.2.18 + 1.3.7 io.netty netty-common - 4.1.138.Final + 4.2.19.Final @@ -161,7 +182,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test @@ -209,17 +230,21 @@ io.netty:netty-tcnative-boringssl-static:[2.0.84.Final] - io.projectreactor.netty:reactor-netty-http:[1.2.18] - io.netty:netty-buffer:[4.1.138.Final] - io.netty:netty-common:[4.1.138.Final] - io.netty:netty-codec:[4.1.138.Final] - io.netty:netty-codec-http:[4.1.138.Final] - io.netty:netty-codec-http2:[4.1.138.Final] - io.netty:netty-handler:[4.1.138.Final] - io.netty:netty-handler-proxy:[4.1.138.Final] - io.netty:netty-transport-native-unix-common:[4.1.138.Final] - io.netty:netty-transport-native-epoll:[4.1.138.Final] - io.netty:netty-transport-native-kqueue:[4.1.138.Final] + io.projectreactor.netty:reactor-netty-http:[1.3.7] + io.netty:netty-buffer:[4.2.19.Final] + io.netty:netty-transport:[4.2.19.Final] + io.netty:netty-common:[4.2.19.Final] + io.netty:netty-codec-base:[4.2.19.Final] + io.netty:netty-codec-http:[4.2.19.Final] + io.netty:netty-codec-http2:[4.2.19.Final] + io.netty:netty-codec-http3:[4.2.19.Final] + io.netty:netty-handler:[4.2.19.Final] + io.netty:netty-handler-proxy:[4.2.19.Final] + io.netty:netty-resolver-dns:[4.2.19.Final] + io.netty:netty-resolver-dns-native-macos:[4.2.19.Final] + io.netty:netty-transport-native-unix-common:[4.2.19.Final] + io.netty:netty-transport-native-epoll:[4.2.19.Final] + io.netty:netty-transport-native-kqueue:[4.2.19.Final] diff --git a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilder.java b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilder.java index 1ee286d12e3ac..2909fb0cd2926 100644 --- a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilder.java +++ b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilder.java @@ -16,12 +16,13 @@ import com.azure.core.util.Context; import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; +import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.logging.LoggingHandler; import io.netty.resolver.DefaultAddressResolverGroup; import io.netty.resolver.NoopAddressResolverGroup; +import io.netty.util.internal.SystemPropertyUtil; import reactor.netty.Connection; import reactor.netty.NettyPipeline; import reactor.netty.http.HttpDecoderSpec; @@ -151,7 +152,7 @@ public NettyAsyncHttpClientBuilder() { * .wiretap(TcpClient.class.getName(), LogLevel.INFO); * // Create an HttpClient based on above reactor-netty client and configure EventLoop count. * HttpClient client = new NettyAsyncHttpClientBuilder(baseHttpClient) - * .eventLoopGroup(new NioEventLoopGroup(5)) + * .eventLoopGroup(new MultiThreadIoEventLoopGroup(5, NioIoHandler.newFactory())) * .build(); * * @@ -202,6 +203,16 @@ public com.azure.core.http.HttpClient build() { nettyHttpClient = HttpClient.create(); } + // Keep the pooled default while respecting allocator choices made by the application. + if (!nettyHttpClient.configuration().options().containsKey(ChannelOption.ALLOCATOR) + && !SystemPropertyUtil.contains("io.netty.allocator.type")) { + // If ChannelOptions and io.netty.allocator.type wasn't configured with an allocator fallback to the default + // setting from Netty 4.1 which was the pooled allocator. Otherwise, respect the configuration, be it + // ChannelOptions or io.netty.allocator.type. In the future, this should be validated for if this should use + // the new Netty 4.2 default which is an adaptive allocator. + nettyHttpClient = nettyHttpClient.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + } + // If a resolver hasn't been set, set the default one. if (nettyHttpClient.configuration().resolver() == null) { nettyHttpClient = nettyHttpClient.resolver(DefaultAddressResolverGroup.INSTANCE); @@ -385,12 +396,12 @@ public NettyAsyncHttpClientBuilder port(int port) { /** * Sets the NIO event loop group that will be used to run IO loops. * - * @param nioEventLoopGroup The {@link NioEventLoopGroup} that will run IO loops. + * @param nioEventLoopGroup The {@link io.netty.channel.nio.NioEventLoopGroup} that will run IO loops. * @return the updated NettyAsyncHttpClientBuilder object. * @deprecated deprecated in favor of {@link #eventLoopGroup(EventLoopGroup)}. */ @Deprecated - public NettyAsyncHttpClientBuilder nioEventLoopGroup(NioEventLoopGroup nioEventLoopGroup) { + public NettyAsyncHttpClientBuilder nioEventLoopGroup(io.netty.channel.nio.NioEventLoopGroup nioEventLoopGroup) { this.eventLoopGroup = nioEventLoopGroup; return this; } @@ -404,7 +415,7 @@ public NettyAsyncHttpClientBuilder nioEventLoopGroup(NioEventLoopGroup nioEventL *

      * int threadCount = 5;
      * HttpClient client = new NettyAsyncHttpClientBuilder()
-     *     .eventLoopGroup(new NioEventLoopGroup(threadCount))
+     *     .eventLoopGroup(new MultiThreadIoEventLoopGroup(threadCount, NioIoHandler.newFactory()))
      *     .build();
      * 
* diff --git a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/ByteBufWriteSubscriber.java b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/ByteBufWriteSubscriber.java index a827169f0e2b6..4b4fb0b6c7ef6 100644 --- a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/ByteBufWriteSubscriber.java +++ b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/ByteBufWriteSubscriber.java @@ -23,6 +23,7 @@ public class ByteBufWriteSubscriber implements Subscriber { private final MonoSink emitter; private final int bufferSize; private final ByteBuf buffer; + private final boolean useHeapBuffer; // This subscriber is effectively synchronous so there is no need for these fields to be volatile. private Subscription subscription; @@ -36,12 +37,28 @@ public class ByteBufWriteSubscriber implements Subscriber { * @param bodySize The size of the request body, if known. */ public ByteBufWriteSubscriber(ExceptionThrowingConsumer writer, MonoSink emitter, Long bodySize) { + this(writer, emitter, bodySize, false); + } + + /** + * Creates a new {@link ByteBufWriteSubscriber} with control over direct buffer writes. + * + * @param writer Where to write the {@link ByteBuf ByteBufs}. + * @param emitter {@link MonoSink} to emit completion or error signals. + * @param bodySize The size of the request body, if known. + * @param useHeapBuffer Whether to copy all writes through a bounded heap buffer. + */ + public ByteBufWriteSubscriber(ExceptionThrowingConsumer writer, MonoSink emitter, Long bodySize, + boolean useHeapBuffer) { this.writer = writer; this.emitter = emitter; + this.useHeapBuffer = useHeapBuffer; // Create a writing buffer that has a minimum bound of 8KB and a maximum bound of 64KB. // This is safe as the writer performs writes synchronously. this.bufferSize = (bodySize == null) ? 65536 : (int) Math.max(8192, Math.min(bodySize, 65536)); - this.buffer = PooledByteBufAllocator.DEFAULT.buffer(bufferSize); + this.buffer = useHeapBuffer + ? PooledByteBufAllocator.DEFAULT.heapBuffer(bufferSize) + : PooledByteBufAllocator.DEFAULT.buffer(bufferSize); } @Override @@ -69,11 +86,25 @@ public void onNext(ByteBuf bytes) { return; } - if (bytes.readableBytes() > bufferSize) { + if (useHeapBuffer) { + while (bytes.isReadable()) { + buffer.writeBytes(bytes, Math.min(bytes.readableBytes(), buffer.writableBytes())); + if (!buffer.isWritable()) { + write(buffer); + if (done) { + return; + } + buffer.clear(); + } + } + } else if (bytes.readableBytes() > bufferSize) { // If the next ByteBuf is larger than the buffer write the buffer, if there is any data to write, then // write the passed ByteBuf without buffering. if (buffer.readableBytes() > 0) { write(buffer); + if (done) { + return; + } buffer.clear(); } @@ -84,6 +115,9 @@ public void onNext(ByteBuf bytes) { } else { // If the next ByteBuf can't fit in the buffer write the buffer then buffer the passed ByteBuf. write(buffer); + if (done) { + return; + } buffer.clear(); buffer.writeBytes(bytes); } diff --git a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyAsyncHttpResponse.java b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyAsyncHttpResponse.java index 0634e15241261..080aec218ac09 100644 --- a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyAsyncHttpResponse.java +++ b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyAsyncHttpResponse.java @@ -80,9 +80,9 @@ public Mono getBodyAsInputStream() { @Override public Mono writeBodyToAsync(AsynchronousByteChannel channel) { Long length = getContentLength(); - return Mono.using(() -> this, - response -> Mono.create(sink -> response.bodyIntern() - .subscribe(new ByteBufWriteSubscriber(byteBuffer -> channel.write(byteBuffer).get(), sink, length))), + // Asynchronous file channels cannot accept scoped direct buffers allocated by Netty on newer JDKs. + return Mono.using(() -> this, response -> Mono.create(sink -> response.bodyIntern() + .subscribe(new ByteBufWriteSubscriber(byteBuffer -> channel.write(byteBuffer).get(), sink, length, true))), NettyAsyncHttpResponse::close); } diff --git a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyUtility.java b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyUtility.java index a0eba784b12f7..233bca94b296e 100644 --- a/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyUtility.java +++ b/sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyUtility.java @@ -36,8 +36,10 @@ public final class NettyUtility { // List of Netty artifacts that should match the 'netty.version' property in the pom.xml file. // Non-native dependencies are required while native dependencies are optional. Without the native dependencies // the SDK will fall back to using the JDK implementations. - private static final List REQUIRED_NETTY_VERSION_ARTIFACTS = Arrays.asList("netty-common", "netty-handler", - "netty-handler-proxy", "netty-buffer", "netty-codec", "netty-codec-http", "netty-codec-http2"); + private static final List REQUIRED_NETTY_VERSION_ARTIFACTS + = Arrays.asList("netty-common", "netty-handler", "netty-handler-proxy", "netty-buffer", "netty-codec-base", + "netty-codec-compression", "netty-codec-dns", "netty-codec-http", "netty-codec-http2", "netty-codec-socks", + "netty-resolver", "netty-resolver-dns", "netty-transport"); private static final List OPTIONAL_NETTY_VERSION_ARTIFACTS = Arrays .asList("netty-transport-native-unix-common", "netty-transport-native-epoll", "netty-transport-native-kqueue"); diff --git a/sdk/core/azure-core-http-netty/src/main/java/module-info.java b/sdk/core/azure-core-http-netty/src/main/java/module-info.java index 30d30814abe41..4ac1c23b8ae0f 100644 --- a/sdk/core/azure-core-http-netty/src/main/java/module-info.java +++ b/sdk/core/azure-core-http-netty/src/main/java/module-info.java @@ -5,11 +5,14 @@ requires transitive com.azure.core; requires io.netty.buffer; requires io.netty.codec; + requires io.netty.codec.compression; requires io.netty.codec.http; + requires io.netty.codec.http2; requires io.netty.common; requires io.netty.handler; requires io.netty.handler.proxy; requires io.netty.resolver; + requires io.netty.resolver.dns; requires io.netty.transport; requires reactor.netty.core; requires reactor.netty.http; diff --git a/sdk/core/azure-core-http-netty/src/main/resources/META-INF/native-image/com.azure/azure-core-http-netty/native-image.properties b/sdk/core/azure-core-http-netty/src/main/resources/META-INF/native-image/com.azure/azure-core-http-netty/native-image.properties index 58d7ea86ca259..86c33a53652fa 100644 --- a/sdk/core/azure-core-http-netty/src/main/resources/META-INF/native-image/com.azure/azure-core-http-netty/native-image.properties +++ b/sdk/core/azure-core-http-netty/src/main/resources/META-INF/native-image/com.azure/azure-core-http-netty/native-image.properties @@ -12,7 +12,6 @@ Args=\ io.netty.channel.epoll.Native,\ io.netty.channel.kqueue.KQueue,\ io.netty.channel.kqueue.KQueueEventArray,\ - io.netty.channel.kqueue.KQueueEventLoop,\ io.netty.channel.kqueue.Native,\ io.netty.channel.unix.Errors,\ io.netty.channel.unix.IovArray,\ @@ -24,10 +23,6 @@ Args=\ io.netty.handler.codec.http.websocketx.WebSocket00FrameEncoder,\ io.netty.handler.ssl.BouncyCastleAlpnSslUtils,\ io.netty.handler.ssl.ConscryptAlpnSslEngine,\ - io.netty.handler.ssl.JdkNpnApplicationProtocolNegotiator,\ - io.netty.handler.ssl.JettyAlpnSslEngine$ClientEngine,\ - io.netty.handler.ssl.JettyAlpnSslEngine$ServerEngine,\ - io.netty.handler.ssl.JettyNpnSslEngine,\ io.netty.handler.ssl.OpenSsl,\ io.netty.handler.ssl.OpenSslAsyncPrivateKeyMethod,\ io.netty.handler.ssl.OpenSslPrivateKeyMethod,\ @@ -44,4 +39,3 @@ Args=\ io.netty.internal.tcnative.SSLPrivateKeyMethod,\ io.netty.util.AbstractReferenceCounted,\ io.netty.util.internal.logging.Log4JLogger - diff --git a/sdk/core/azure-core-http-netty/src/samples/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilderJavaDocCodeSnippets.java b/sdk/core/azure-core-http-netty/src/samples/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilderJavaDocCodeSnippets.java index 348e8b1b20f89..0d60c8e6b6f22 100644 --- a/sdk/core/azure-core-http-netty/src/samples/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilderJavaDocCodeSnippets.java +++ b/sdk/core/azure-core-http-netty/src/samples/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilderJavaDocCodeSnippets.java @@ -7,7 +7,8 @@ import com.azure.core.http.HttpMethod; import com.azure.core.http.HttpRequest; import com.azure.core.http.ProxyOptions; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.handler.logging.LogLevel; import reactor.netty.resources.ConnectionProvider; import reactor.netty.tcp.TcpClient; @@ -39,7 +40,7 @@ public void fixedThreadPoolSample() { // BEGIN: com.azure.core.http.netty.NettyAsyncHttpClientBuilder#eventLoopGroup int threadCount = 5; HttpClient client = new NettyAsyncHttpClientBuilder() - .eventLoopGroup(new NioEventLoopGroup(threadCount)) + .eventLoopGroup(new MultiThreadIoEventLoopGroup(threadCount, NioIoHandler.newFactory())) .build(); // END: com.azure.core.http.netty.NettyAsyncHttpClientBuilder#eventLoopGroup } @@ -87,7 +88,7 @@ public void fromExistingReactorNettyClient() { .wiretap(TcpClient.class.getName(), LogLevel.INFO); // Create an HttpClient based on above reactor-netty client and configure EventLoop count. HttpClient client = new NettyAsyncHttpClientBuilder(baseHttpClient) - .eventLoopGroup(new NioEventLoopGroup(5)) + .eventLoopGroup(new MultiThreadIoEventLoopGroup(5, NioIoHandler.newFactory())) .build(); // END: com.azure.core.http.netty.from-existing-http-client } diff --git a/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilderTests.java b/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilderTests.java index c6a4f68e1b0e8..e93d31a8978ce 100644 --- a/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilderTests.java +++ b/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/NettyAsyncHttpClientBuilderTests.java @@ -11,17 +11,23 @@ import com.azure.core.util.Configuration; import com.azure.core.util.ConfigurationBuilder; import com.azure.core.util.ConfigurationSource; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPromise; -import io.netty.channel.nio.NioEventLoop; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.SingleThreadIoEventLoop; +import io.netty.channel.nio.NioIoHandler; import io.netty.handler.codec.http.cookie.DefaultCookie; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.proxy.HttpProxyHandler; import io.netty.handler.proxy.ProxyHandler; import io.netty.handler.proxy.Socks4ProxyHandler; import io.netty.handler.proxy.Socks5ProxyHandler; +import io.netty.util.internal.SystemPropertyUtil; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; @@ -52,6 +58,7 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -104,6 +111,24 @@ public void startingWithNullClientThrows() { assertThrows(NullPointerException.class, () -> new NettyAsyncHttpClientBuilder(null)); } + @Test + public void defaultAllocatorRespectsSystemProperty() { + NettyAsyncHttpClient client = (NettyAsyncHttpClient) new NettyAsyncHttpClientBuilder().build(); + ByteBufAllocator expectedAllocator = SystemPropertyUtil.contains("io.netty.allocator.type") + ? ByteBufAllocator.DEFAULT + : PooledByteBufAllocator.DEFAULT; + Object configuredAllocator = client.nettyClient.configuration().options().get(ChannelOption.ALLOCATOR); + assertSame(expectedAllocator, configuredAllocator == null ? ByteBufAllocator.DEFAULT : configuredAllocator); + } + + @Test + public void preconfiguredAllocatorIsMaintained() { + HttpClient baseClient = HttpClient.create().option(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT); + NettyAsyncHttpClient client = (NettyAsyncHttpClient) new NettyAsyncHttpClientBuilder(baseClient).build(); + assertSame(UnpooledByteBufAllocator.DEFAULT, + client.nettyClient.configuration().options().get(ChannelOption.ALLOCATOR)); + } + /** * Tests that creating a client with a {@link ConnectionProvider} will use it to create connections to a server. */ @@ -265,6 +290,8 @@ private static Stream buildWithEnvConfigurationProxySupplier() { Configuration simpleEnvProxy = new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, new TestConfigurationSource().put(Configuration.PROPERTY_HTTP_PROXY, "http://localhost:12345") + .put(Configuration.PROPERTY_HTTPS_PROXY, "") + .put(Configuration.PROPERTY_NO_PROXY, "") .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true")).build(); arguments.add(Arguments.of(true, false, simpleEnvProxy, DEFAULT_URL)); @@ -279,6 +306,8 @@ private static Stream buildWithEnvConfigurationProxySupplier() { Configuration envProxyWithAuthentication = new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, new TestConfigurationSource().put(Configuration.PROPERTY_HTTP_PROXY, "http://1:1@localhost:12345") + .put(Configuration.PROPERTY_HTTPS_PROXY, "") + .put(Configuration.PROPERTY_NO_PROXY, "") .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true")).build(); arguments.add(Arguments.of(true, true, envProxyWithAuthentication, DEFAULT_URL)); @@ -304,6 +333,7 @@ private static Stream buildWithEnvConfigurationProxySupplier() { = () -> baseJavaProxyConfigurationSupplier.get().put(JAVA_NON_PROXY_HOSTS, rawJavaNonProxyHosts); Supplier envNonProxyHostsSupplier = () -> new TestConfigurationSource().put(Configuration.PROPERTY_HTTP_PROXY, "http://localhost:12345") + .put(Configuration.PROPERTY_HTTPS_PROXY, "") .put(Configuration.PROPERTY_NO_PROXY, rawEnvNonProxyHosts) .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true"); @@ -331,6 +361,7 @@ private static Stream buildWithEnvConfigurationProxySupplier() { = () -> javaNonProxyHostsSupplier.get().put(JAVA_HTTP_PROXY_USER, "1").put(JAVA_HTTP_PROXY_PASSWORD, "1"); Supplier authenticatedEnvNonProxyHostsSupplier = () -> new TestConfigurationSource().put(Configuration.PROPERTY_HTTP_PROXY, "http://1:1@localhost:12345") + .put(Configuration.PROPERTY_HTTPS_PROXY, "") .put(Configuration.PROPERTY_NO_PROXY, rawEnvNonProxyHosts) .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true"); @@ -524,22 +555,26 @@ public void buildPortClient() { public void buildEventLoopClient() { String expectedThreadName = "testEventLoop"; HttpClient validatorClient = HttpClient.create().doAfterResponseSuccess((response, connection) -> { - // Validate that the EventLoop being used is a NioEventLoop. - NioEventLoop eventLoop = (NioEventLoop) connection.channel().eventLoop(); + SingleThreadIoEventLoop eventLoop = (SingleThreadIoEventLoop) connection.channel().eventLoop(); assertNotNull(eventLoop); assertEquals(expectedThreadName, eventLoop.threadProperties().name()); }); - NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup(1, (Runnable r) -> new Thread(r, expectedThreadName)); + MultiThreadIoEventLoopGroup eventLoopGroup = new MultiThreadIoEventLoopGroup(1, + (Runnable r) -> new Thread(r, expectedThreadName), NioIoHandler.newFactory()); - NettyAsyncHttpClient nettyClient - = (NettyAsyncHttpClient) new NettyAsyncHttpClientBuilder(validatorClient).eventLoopGroup(eventLoopGroup) - .build(); + try { + NettyAsyncHttpClient nettyClient + = (NettyAsyncHttpClient) new NettyAsyncHttpClientBuilder(validatorClient).eventLoopGroup(eventLoopGroup) + .build(); - StepVerifier.create(nettyClient.send(new HttpRequest(HttpMethod.GET, DEFAULT_URL))) - .assertNext(response -> assertEquals(200, response.getStatusCode())) - .verifyComplete(); + StepVerifier.create(nettyClient.send(new HttpRequest(HttpMethod.GET, DEFAULT_URL))) + .assertNext(response -> assertEquals(200, response.getStatusCode())) + .verifyComplete(); + } finally { + eventLoopGroup.shutdownGracefully().syncUninterruptibly(); + } } @ParameterizedTest diff --git a/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/NettyNativeTransportModulesTests.java b/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/NettyNativeTransportModulesTests.java new file mode 100644 index 0000000000000..198b9d5bfc8ed --- /dev/null +++ b/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/NettyNativeTransportModulesTests.java @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.http.netty; + +import io.netty.util.Version; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies native module resolution without loading platform-specific JNI libraries. + */ +@EnabledForJreRange(min = JRE.JAVA_11) +public class NettyNativeTransportModulesTests { + private static final String EPOLL_CLASSES = "io.netty.transport.classes.epoll"; + private static final String EPOLL_NATIVE = "io.netty.transport.epoll.linux.x86_64"; + private static final String KQUEUE_CLASSES = "io.netty.transport.classes.kqueue"; + private static final String KQUEUE_NATIVE = "io.netty.transport.kqueue.osx.x86_64"; + + @ParameterizedTest + @MethodSource("nativeTransports") + @Timeout(value = 30, unit = TimeUnit.SECONDS) + public void nativeTransportRequiresBothModuleRoots(String classesModule, String nativeModule) throws Exception { + String withoutRoots = resolveModules("io.netty.transport"); + assertFalse(isResolved(withoutRoots, classesModule), withoutRoots); + assertFalse(isResolved(withoutRoots, nativeModule), withoutRoots); + + String withClasses = resolveModules("io.netty.transport", classesModule); + assertTrue(isResolved(withClasses, classesModule), withClasses); + assertFalse(isResolved(withClasses, nativeModule), withClasses); + + String withBoth = resolveModules("io.netty.transport", classesModule, nativeModule); + assertTrue(isResolved(withBoth, classesModule), withBoth); + assertTrue(isResolved(withBoth, nativeModule), withBoth); + } + + private static Stream nativeTransports() { + return Stream.of(Arguments.of(EPOLL_CLASSES, EPOLL_NATIVE), Arguments.of(KQUEUE_CLASSES, KQUEUE_NATIVE)); + } + + private static boolean isResolved(String output, String name) { + return output.contains("root " + name + " ") + || output.contains(" requires " + name + " ") + || output.contains(" binds " + name + " "); + } + + private static String resolveModules(String... roots) throws Exception { + String modulePath = modulePath(); + String executable + = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win") ? "java.exe" : "java"; + String java = Paths.get(System.getProperty("java.home"), "bin", executable).toString(); + List command = Arrays.asList(java, "--module-path", modulePath, "--add-modules", + String.join(",", roots), "--show-module-resolution", "-version"); + + Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (InputStream input = process.getInputStream()) { + byte[] buffer = new byte[4096]; + int count; + while ((count = input.read(buffer)) != -1) { + output.write(buffer, 0, count); + } + } + + String text = new String(output.toByteArray(), StandardCharsets.UTF_8); + assertEquals(0, process.waitFor(), text); + return text; + } + + private static String modulePath() { + Version nettyCommon + = Version.identify(NettyNativeTransportModulesTests.class.getClassLoader()).get("netty-common"); + assertNotNull(nettyCommon, "Missing Netty common version"); + String version = nettyCommon.artifactVersion(); + // Surefire separates resolved modules from the test classpath containing the optional native JARs. + List entries = Stream.of("surefire.test.class.path", "jdk.module.path", "java.class.path") + .map(System::getProperty) + .filter(value -> value != null && !value.isEmpty()) + .flatMap(value -> Arrays.stream(value.split(Pattern.quote(File.pathSeparator)))) + .map(Paths::get) + .distinct() + .collect(Collectors.toList()); + assertFalse(entries.isEmpty(), "Missing test classpath and module path"); + List jars = Arrays.asList(jarFromClasspath(entries, "netty-common-" + version + ".jar"), + jarFromClasspath(entries, "netty-buffer-" + version + ".jar"), + jarFromClasspath(entries, "netty-transport-" + version + ".jar"), + jarFromClasspath(entries, "netty-resolver-" + version + ".jar"), + jarFromClasspath(entries, "netty-transport-native-unix-common-" + version + ".jar"), + jarFromClasspath(entries, "netty-transport-classes-epoll-" + version + ".jar"), + jarFromClasspath(entries, "netty-transport-classes-kqueue-" + version + ".jar"), + jarFromClasspath(entries, "netty-transport-native-epoll-" + version + "-linux-x86_64.jar"), + jarFromClasspath(entries, "netty-transport-native-kqueue-" + version + "-osx-x86_64.jar")); + return jars.stream().distinct().map(Path::toString).collect(Collectors.joining(File.pathSeparator)); + } + + private static Path jarFromClasspath(List entries, String fileName) { + List matches = entries.stream() + .filter(path -> path.getFileName().toString().equals(fileName)) + .collect(Collectors.toList()); + assertEquals(1, matches.size(), "Expected one Netty test dependency for " + fileName); + return matches.get(0); + } +} diff --git a/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/implementation/ByteBufWriterSubscriberTests.java b/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/implementation/ByteBufWriterSubscriberTests.java index 752bc322d0a4d..988ba5467941a 100644 --- a/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/implementation/ByteBufWriterSubscriberTests.java +++ b/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/implementation/ByteBufWriterSubscriberTests.java @@ -6,11 +6,13 @@ import com.azure.core.http.netty.mocking.MockMonoSink; import com.azure.core.http.netty.mocking.WriteCountTrackingChannel; import io.netty.buffer.ByteBuf; +import io.netty.buffer.PooledByteBufAllocator; import io.netty.buffer.Unpooled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,14 +32,55 @@ import static com.azure.core.validation.http.HttpValidatonUtils.assertArraysEqual; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests {@link ByteBufWriteSubscriber}. */ @SuppressWarnings("resource") public class ByteBufWriterSubscriberTests { + @ParameterizedTest + @ValueSource(ints = { 4096, 65536, 131136 }) + public void heapBufferWritesDirectBuffersInBoundedChunks(int size) { + byte[] expected = new byte[size]; + ThreadLocalRandom.current().nextBytes(expected); + ByteBuf data = PooledByteBufAllocator.DEFAULT.directBuffer(size).writeBytes(expected); + WriteCountTrackingChannel channel = new WriteCountTrackingChannel(); + try { + StepVerifier + .create(Mono.create(sink -> Flux.just(data).subscribe(new ByteBufWriteSubscriber(buffer -> { + assertFalse(buffer.isDirect()); + assertTrue(buffer.remaining() <= 65536); + channel.write(buffer); + }, sink, null, true)))) + .verifyComplete(); + + assertArraysEqual(expected, channel.getDataWritten()); + } finally { + data.release(); + } + } + + @Test + public void heapBufferWriteFailureStopsBeforeWritingRemainingData() { + ByteBuf data = PooledByteBufAllocator.DEFAULT.directBuffer(131136).writeZero(131136); + AtomicInteger writes = new AtomicInteger(); + try { + StepVerifier + .create(Mono.create(sink -> Flux.just(data).subscribe(new ByteBufWriteSubscriber(buffer -> { + writes.incrementAndGet(); + throw new IOException("write failed"); + }, sink, null, true)))) + .verifyError(IOException.class); + assertEquals(1, writes.get()); + } finally { + data.release(); + } + } + /** * Tests that when multiple subscriptions are made to the subscriber all subscriptions after the first are ignored * and cancelled. diff --git a/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/implementation/NettyUtilityTests.java b/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/implementation/NettyUtilityTests.java index 5a71bba892f80..0b6013caa7846 100644 --- a/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/implementation/NettyUtilityTests.java +++ b/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/implementation/NettyUtilityTests.java @@ -23,6 +23,10 @@ public void validateNettyVersionsWithWhatThePomSpecifies() { // Should never have version mismatches when running tests, that would mean either the version properties are // wrong or there is a dependency diamond within azure-core-http-netty. Either way, it should be fixed. assertFalse(logInformation.shouldLog()); + assertTrue(logInformation.classpathNettyVersions.containsKey("io.netty:netty-codec-base")); + assertTrue(logInformation.classpathNettyVersions.containsKey("io.netty:netty-codec-compression")); + assertTrue(logInformation.classpathNettyVersions.containsKey("io.netty:netty-codec-socks")); + assertFalse(logInformation.classpathNettyVersions.containsKey("io.netty:netty-codec")); for (String artifactFullName : logInformation.classpathNettyVersions.keySet()) { assertTrue(artifactFullName.startsWith("io.netty:netty-"), "All artifact information should start with 'io.netty:netty-'"); diff --git a/sdk/core/azure-core-http-okhttp/pom.xml b/sdk/core/azure-core-http-okhttp/pom.xml index 7ce74e03bff27..9c20a28e20b14 100644 --- a/sdk/core/azure-core-http-okhttp/pom.xml +++ b/sdk/core/azure-core-http-okhttp/pom.xml @@ -108,7 +108,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/core/azure-core-http-okhttp/src/test/java/com/azure/core/http/okhttp/OkHttpAsyncHttpClientBuilderTests.java b/sdk/core/azure-core-http-okhttp/src/test/java/com/azure/core/http/okhttp/OkHttpAsyncHttpClientBuilderTests.java index 066cfd98590cf..79b98573d01f2 100644 --- a/sdk/core/azure-core-http-okhttp/src/test/java/com/azure/core/http/okhttp/OkHttpAsyncHttpClientBuilderTests.java +++ b/sdk/core/azure-core-http-okhttp/src/test/java/com/azure/core/http/okhttp/OkHttpAsyncHttpClientBuilderTests.java @@ -455,6 +455,8 @@ private static Stream buildWithEnvConfigurationProxySupplier() { Configuration simpleEnvProxy = new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, new TestConfigurationSource().put(Configuration.PROPERTY_HTTP_PROXY, "http://localhost:12345") + .put(Configuration.PROPERTY_HTTPS_PROXY, "") + .put(Configuration.PROPERTY_NO_PROXY, "") .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true")).build(); arguments.add(Arguments.of(true, simpleEnvProxy, SERVER_HTTP_URI + DEFAULT_PATH)); @@ -468,6 +470,8 @@ private static Stream buildWithEnvConfigurationProxySupplier() { Configuration envProxyWithAuthentication = new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, new TestConfigurationSource().put(Configuration.PROPERTY_HTTP_PROXY, "http://1:1@localhost:12345") + .put(Configuration.PROPERTY_HTTPS_PROXY, "") + .put(Configuration.PROPERTY_NO_PROXY, "") .put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true")).build(); arguments.add(Arguments.of(true, envProxyWithAuthentication, SERVER_HTTP_URI + DEFAULT_PATH)); diff --git a/sdk/core/azure-core-http-vertx/README.md b/sdk/core/azure-core-http-vertx/README.md index 791822d6f457d..b41666349386e 100644 --- a/sdk/core/azure-core-http-vertx/README.md +++ b/sdk/core/azure-core-http-vertx/README.md @@ -10,6 +10,10 @@ Azure Core Vert.x HTTP client is a plugin for the `azure-core` HTTP client API. - Here are details about [Java 8 client compatibility with Azure Certificate Authority][java8_client_compatibility]. ### Include the package + +This transport remains on Vert.x 4.5 and Netty 4.1 to support Java 8. Do not combine it with the Netty 4.2-based +`azure-core-http-netty` library on the same classpath. + #### Include the BOM file Please include the azure-sdk-bom to your project to take dependency on the General Availability (GA) version of the library. In the following snippet, replace the {bom_version_to_target} placeholder with the version number. @@ -137,4 +141,3 @@ For details on contributing to this repository, see the [contributing guide](htt [jdk_link]: https://learn.microsoft.com/java/azure/jdk/?view=azure-java-stable [java8_client_compatibility]: https://learn.microsoft.com/azure/security/fundamentals/azure-ca-details?tabs=root-and-subordinate-cas-list#client-compatibility-for-public-pkis - diff --git a/sdk/core/azure-core-http-vertx/pom.xml b/sdk/core/azure-core-http-vertx/pom.xml index ff9ad81b5b828..8e0d2b3bfdc16 100644 --- a/sdk/core/azure-core-http-vertx/pom.xml +++ b/sdk/core/azure-core-http-vertx/pom.xml @@ -104,7 +104,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/core/azure-core-http-vertx/src/test/java/com/azure/core/http/vertx/VertxHttpClientBuilderTests.java b/sdk/core/azure-core-http-vertx/src/test/java/com/azure/core/http/vertx/VertxHttpClientBuilderTests.java index ecc6f3ce4c0cb..5505ad215efc1 100644 --- a/sdk/core/azure-core-http-vertx/src/test/java/com/azure/core/http/vertx/VertxHttpClientBuilderTests.java +++ b/sdk/core/azure-core-http-vertx/src/test/java/com/azure/core/http/vertx/VertxHttpClientBuilderTests.java @@ -131,11 +131,10 @@ public void buildWithHttpProxy() { @Test public void buildWithHttpProxyFromEnvConfiguration() { - Configuration configuration = new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, - new TestConfigurationSource() - .put(Configuration.PROPERTY_HTTP_PROXY, - "http://" + PROXY_USER_INFO + "localhost:" + PROXY_SERVER_HTTP_PORT) - .put("java.net.useSystemProxies", "true")).build(); + Configuration configuration = new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, new TestConfigurationSource() + .put(Configuration.PROPERTY_HTTP_PROXY, "http://" + PROXY_USER_INFO + "localhost:" + PROXY_SERVER_HTTP_PORT) + .put(Configuration.PROPERTY_HTTPS_PROXY, "") + .put("java.net.useSystemProxies", "true")).build(); HttpClient httpClient = new VertxHttpClientBuilder().configuration(configuration).build(); diff --git a/sdk/core/azure-core-management/pom.xml b/sdk/core/azure-core-management/pom.xml index 33db44dfc2e0e..545c0e33af1e6 100644 --- a/sdk/core/azure-core-management/pom.xml +++ b/sdk/core/azure-core-management/pom.xml @@ -103,7 +103,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/core/azure-core-perf/pom.xml b/sdk/core/azure-core-perf/pom.xml index 89055c6cf7044..69db6ff1dd694 100644 --- a/sdk/core/azure-core-perf/pom.xml +++ b/sdk/core/azure-core-perf/pom.xml @@ -63,7 +63,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/core/azure-core-serializer-avro-apache/pom.xml b/sdk/core/azure-core-serializer-avro-apache/pom.xml index 155ecd39c844b..523385f0938f1 100644 --- a/sdk/core/azure-core-serializer-avro-apache/pom.xml +++ b/sdk/core/azure-core-serializer-avro-apache/pom.xml @@ -102,7 +102,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/core/azure-core-serializer-json-gson/pom.xml b/sdk/core/azure-core-serializer-json-gson/pom.xml index c231056934ef4..7eee2e35024bb 100644 --- a/sdk/core/azure-core-serializer-json-gson/pom.xml +++ b/sdk/core/azure-core-serializer-json-gson/pom.xml @@ -118,7 +118,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/core/azure-core-serializer-json-jackson/pom.xml b/sdk/core/azure-core-serializer-json-jackson/pom.xml index 13449a593c00b..8bd0b67033105 100644 --- a/sdk/core/azure-core-serializer-json-jackson/pom.xml +++ b/sdk/core/azure-core-serializer-json-jackson/pom.xml @@ -148,7 +148,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/core/azure-core-test/CHANGELOG.md b/sdk/core/azure-core-test/CHANGELOG.md index 22e0eb716ca7d..55fab4183b2ef 100644 --- a/sdk/core/azure-core-test/CHANGELOG.md +++ b/sdk/core/azure-core-test/CHANGELOG.md @@ -10,6 +10,10 @@ ### Other Changes +#### Dependency Updates + +- Upgraded Reactor to `3.8.7` and the shared Netty transport to Netty `4.2.17.Final` / Reactor Netty `1.3.7`. + ## 1.27.0-beta.19 (2026-09-29) ### Other Changes diff --git a/sdk/core/azure-core-test/README.md b/sdk/core/azure-core-test/README.md index 34b976af00036..ec70fbbe47911 100644 --- a/sdk/core/azure-core-test/README.md +++ b/sdk/core/azure-core-test/README.md @@ -353,4 +353,3 @@ This project has adopted the [Microsoft Open Source Code of Conduct][coc]. For m [coc]: https://opensource.microsoft.com/codeofconduct/ [coc_faq]: https://opensource.microsoft.com/codeofconduct/faq/ - diff --git a/sdk/core/azure-core-test/pom.xml b/sdk/core/azure-core-test/pom.xml index 5448fb6a82c03..af2ef000ae484 100644 --- a/sdk/core/azure-core-test/pom.xml +++ b/sdk/core/azure-core-test/pom.xml @@ -99,7 +99,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 @@ -142,7 +142,7 @@ org.junit.jupiter:junit-jupiter-api:[5.13.4] org.junit.jupiter:junit-jupiter-engine:[5.13.4] org.junit.jupiter:junit-jupiter-params:[5.13.4] - io.projectreactor:reactor-test:[3.7.19] + io.projectreactor:reactor-test:[3.8.7] org.apache.ant:ant:[1.10.18] diff --git a/sdk/core/azure-core-tracing-opentelemetry/pom.xml b/sdk/core/azure-core-tracing-opentelemetry/pom.xml index d0da878d38261..f47d65bdda496 100644 --- a/sdk/core/azure-core-tracing-opentelemetry/pom.xml +++ b/sdk/core/azure-core-tracing-opentelemetry/pom.xml @@ -100,7 +100,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/core/azure-core-version-tests/pom.xml b/sdk/core/azure-core-version-tests/pom.xml index 4e61ea014b666..424e3335598d0 100644 --- a/sdk/core/azure-core-version-tests/pom.xml +++ b/sdk/core/azure-core-version-tests/pom.xml @@ -95,7 +95,7 @@ io.projectreactor reactor-core - 3.7.19 + 3.8.7 + io.projectreactor:reactor-core:[3.8.7] com.fasterxml.jackson.core:jackson-annotations:[2.18.11] com.fasterxml.jackson.core:jackson-core:[2.18.11] com.fasterxml.jackson.core:jackson-databind:[2.18.11] diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index db524afd49068..1def8c71d7090 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -10,6 +10,10 @@ ### Other Changes +#### Dependency Updates + +- Upgraded Reactor from `3.7.19` to `3.8.7`. + ## 1.60.0 (2026-09-29) ### Features Added @@ -27,6 +31,8 @@ ### Other Changes +#### Dependency Updates + - Upgraded Jackson from `2.18.9` to `2.18.11`. ## 1.59.1 (2026-08-27) diff --git a/sdk/core/azure-core/pom.xml b/sdk/core/azure-core/pom.xml index 4b61b3ed11d9f..8f6487e84de51 100644 --- a/sdk/core/azure-core/pom.xml +++ b/sdk/core/azure-core/pom.xml @@ -146,7 +146,7 @@ io.projectreactor reactor-core - 3.7.19 + 3.8.7 @@ -166,7 +166,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test @@ -223,7 +223,7 @@ - io.projectreactor:reactor-core:[3.7.19] + io.projectreactor:reactor-core:[3.8.7] com.fasterxml.jackson.core:jackson-annotations:[2.18.11] com.fasterxml.jackson.core:jackson-core:[2.18.11] com.fasterxml.jackson.core:jackson-databind:[2.18.11] diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/http/ProxyOptionsTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/http/ProxyOptionsTests.java index 2e0a5e6a6d096..cc5813a589d45 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/http/ProxyOptionsTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/http/ProxyOptionsTests.java @@ -238,53 +238,35 @@ public void invalidHttpPortEnvironmentConfigDefault(String port) { private static Stream loadFromEnvironmentSupplier() { return Stream.of( // Basic Azure HTTPS proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.PROPERTY_HTTPS_PROXY, AZURE_HTTPS_PROXY_HOST_ONLY)), - PROXY_HOST, 443, null, null, null), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTPS_PROXY_HOST_ONLY, null, true), PROXY_HOST, 443, + null, null, null), // Username only Azure HTTPS proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue(new TestConfigurationSource() - .put(Configuration.PROPERTY_HTTPS_PROXY, AZURE_HTTPS_PROXY_WITH_USERNAME)), - PROXY_HOST, 443, null, null, null), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTPS_PROXY_WITH_USERNAME, null, true), PROXY_HOST, + 443, null, null, null), // Complete Azure HTTPS proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue(new TestConfigurationSource() - .put(Configuration.PROPERTY_HTTPS_PROXY, AZURE_HTTPS_PROXY_WITH_USER_AND_PASS)), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTPS_PROXY_WITH_USER_AND_PASS, null, true), PROXY_HOST, 443, FAKE_PROXY_USER_PLACEHOLDER, FAKE_PROXY_PASSWORD_PLACEHOLDER, null), // Azure HTTPS proxy with non-proxying hosts. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.PROPERTY_HTTPS_PROXY, AZURE_HTTPS_PROXY_HOST_ONLY) - .put(Configuration.PROPERTY_NO_PROXY, NON_PROXY_HOSTS)), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTPS_PROXY_HOST_ONLY, NON_PROXY_HOSTS, true), PROXY_HOST, 443, null, null, "(" + NON_PROXY_HOSTS + ")"), // Basic Azure HTTP proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.PROPERTY_HTTP_PROXY, AZURE_HTTP_PROXY_HOST_ONLY)), - PROXY_HOST, 80, null, null, null), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTP_PROXY_HOST_ONLY, null, false), PROXY_HOST, 80, + null, null, null), // Username only Azure HTTP proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue(new TestConfigurationSource() - .put(Configuration.PROPERTY_HTTP_PROXY, AZURE_HTTP_PROXY_WITH_USERNAME)), - PROXY_HOST, 80, null, null, null), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTP_PROXY_WITH_USERNAME, null, false), PROXY_HOST, + 80, null, null, null), // Complete Azure HTTP proxy. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue(new TestConfigurationSource() - .put(Configuration.PROPERTY_HTTP_PROXY, AZURE_HTTP_PROXY_WITH_USER_AND_PASS)), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTP_PROXY_WITH_USER_AND_PASS, null, false), PROXY_HOST, 80, FAKE_PROXY_USER_PLACEHOLDER, FAKE_PROXY_PASSWORD_PLACEHOLDER, null), // Azure HTTP proxy with non-proxying hosts. - Arguments.of( - setJavaSystemProxyPrerequisiteToTrue( - new TestConfigurationSource().put(Configuration.PROPERTY_HTTP_PROXY, AZURE_HTTP_PROXY_HOST_ONLY) - .put(Configuration.PROPERTY_NO_PROXY, NON_PROXY_HOSTS)), + Arguments.of(setJavaSystemProxyPrerequisiteToTrue(AZURE_HTTP_PROXY_HOST_ONLY, NON_PROXY_HOSTS, false), PROXY_HOST, 80, null, null, "(" + NON_PROXY_HOSTS + ")"), /* @@ -328,7 +310,7 @@ private static Stream loadFromEnvironmentSupplier() { private static Stream loadFromExplicitConfigurationSupplier() { return Stream.of( /* - * Setting up tests for loading the Java exokicit proxy configurations takes additional work as each + * Setting up tests for loading the Java explicit proxy configurations takes additional work as each * piece of the proxy configuration is a separate environment value. The non-proxy hosts will be checked * against the global environment value when it is not being set by the configuration passed by the test * as this value may be setup by the JVM. @@ -447,10 +429,8 @@ private static Stream nonProxyHostsSupplier() { /* * This emulates loading an environment formatted proxy. */ - TestConfigurationSource envSource - = new TestConfigurationSource().put(Configuration.PROPERTY_HTTPS_PROXY, "http://localhost:7777") - .put(Configuration.PROPERTY_NO_PROXY, noProxyNonProxyHosts); - Configuration environmentProxyConfiguration = setJavaSystemProxyPrerequisiteToTrue(envSource); + Configuration environmentProxyConfiguration + = setJavaSystemProxyPrerequisiteToTrue("http://localhost:7777", noProxyNonProxyHosts, true); Pattern environmentProxyConfigurationPattern = compile(fromConfiguration(environmentProxyConfiguration).getNonProxyHosts(), Pattern.CASE_INSENSITIVE); @@ -510,8 +490,22 @@ public void sanitizeJavaHttpNonProxyHostsDoesNotSplitEscapedPipes() { assertEquals("(" + nonProxyHosts + ")", ProxyOptions.sanitizeNoProxy(nonProxyHosts)); } - private static Configuration setJavaSystemProxyPrerequisiteToTrue(TestConfigurationSource source) { - return new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, source.put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true")) - .build(); + private static Configuration setJavaSystemProxyPrerequisiteToTrue(String proxy, String nonProxyHosts, + boolean isHttps) { + TestConfigurationSource testSource = new TestConfigurationSource().put(JAVA_SYSTEM_PROXY_PREREQUISITE, "true") + // Use empty string when non-proxy hosts is null or empty to ensure that the environment configuration + // doesn't attempt to fall back to loading from System.properties or System.env. + .put(Configuration.PROPERTY_NO_PROXY, CoreUtils.isNullOrEmpty(nonProxyHosts) ? "" : nonProxyHosts); + + if (isHttps) { + testSource.put(Configuration.PROPERTY_HTTPS_PROXY, proxy); + } else { + testSource.put(Configuration.PROPERTY_HTTP_PROXY, proxy) + // Same as with non-proxy hosts, set HTTPS_PROXY to empty when testing HTTP_PROXY as HTTPS is checked + // first and could fall back to loading from System.properties or System.env. + .put(Configuration.PROPERTY_HTTPS_PROXY, ""); + } + + return new ConfigurationBuilder(EMPTY_SOURCE, EMPTY_SOURCE, testSource).build(); } } diff --git a/sdk/cosmos/azure-cosmos-encryption/pom.xml b/sdk/cosmos/azure-cosmos-encryption/pom.xml index bf5b3d0178031..e0c497f053672 100644 --- a/sdk/cosmos/azure-cosmos-encryption/pom.xml +++ b/sdk/cosmos/azure-cosmos-encryption/pom.xml @@ -51,6 +51,9 @@ Licensed under the MIT License. --add-opens com.azure.cosmos/com.azure.cosmos.implementation=ALL-UNNAMED --add-modules java.management --add-reads com.azure.cosmos.encryption=java.management + --add-reads com.azure.cosmos.encryption=io.netty.common + --add-reads com.azure.cosmos.encryption=io.netty.buffer + --add-reads com.azure.cosmos.encryption=io.netty.codec.http - true @@ -178,7 +181,7 @@ Licensed under the MIT License. io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/cosmos/azure-cosmos-spark-account-data-resolver-sample/pom.xml b/sdk/cosmos/azure-cosmos-spark-account-data-resolver-sample/pom.xml index 93239be5fdb5b..8662d66cd84c7 100644 --- a/sdk/cosmos/azure-cosmos-spark-account-data-resolver-sample/pom.xml +++ b/sdk/cosmos/azure-cosmos-spark-account-data-resolver-sample/pom.xml @@ -159,7 +159,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/cosmos/azure-cosmos-spark_3-4_2-12/pom.xml b/sdk/cosmos/azure-cosmos-spark_3-4_2-12/pom.xml index 7e0bf98d65c7c..bfe17f9cfdfdc 100644 --- a/sdk/cosmos/azure-cosmos-spark_3-4_2-12/pom.xml +++ b/sdk/cosmos/azure-cosmos-spark_3-4_2-12/pom.xml @@ -183,6 +183,14 @@ io.netty netty-all + + io.netty + netty-transport-native-epoll + + + io.netty + netty-transport-native-kqueue + org.slf4j * @@ -199,6 +207,14 @@ io.netty netty-all + + io.netty + netty-transport-native-epoll + + + io.netty + netty-transport-native-kqueue + org.slf4j * diff --git a/sdk/cosmos/azure-cosmos-spark_3-5/pom.xml b/sdk/cosmos/azure-cosmos-spark_3-5/pom.xml index e9918828cbec9..be5c1955b729d 100644 --- a/sdk/cosmos/azure-cosmos-spark_3-5/pom.xml +++ b/sdk/cosmos/azure-cosmos-spark_3-5/pom.xml @@ -104,6 +104,14 @@ io.netty netty-all + + io.netty + netty-transport-native-epoll + + + io.netty + netty-transport-native-kqueue + org.slf4j * @@ -120,6 +128,14 @@ io.netty netty-all + + io.netty + netty-transport-native-epoll + + + io.netty + netty-transport-native-kqueue + org.slf4j * diff --git a/sdk/cosmos/azure-cosmos-spark_3/pom.xml b/sdk/cosmos/azure-cosmos-spark_3/pom.xml index fdc6b8258a842..7f50977a332ae 100644 --- a/sdk/cosmos/azure-cosmos-spark_3/pom.xml +++ b/sdk/cosmos/azure-cosmos-spark_3/pom.xml @@ -89,6 +89,11 @@ scala-java8-compat_${scala.binary.version} ${scala-java8-compat.version} + + io.netty + netty-transport-classes-epoll + 4.2.19.Final + io.projectreactor reactor-scala-extensions_${scala.binary.version} @@ -128,7 +133,7 @@ com.azure azure-core-http-netty - 1.16.8 + 1.17.0-beta.1 com.azure @@ -198,7 +203,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test @@ -317,6 +322,7 @@ org.apache.commons:commons-lang3:[3.18.0] + io.netty:netty-transport-classes-epoll:[4.2.19.Final] org.slf4j:slf4j-api:[1.7.36] org.apache.spark:spark-sql_2.12:[${spark33.version}] org.apache.spark:spark-sql_2.12:[${spark34.version}] diff --git a/sdk/cosmos/azure-cosmos-spark_3/src/test/scala/com/azure/cosmos/spark/NettyCompatibilitySpec.scala b/sdk/cosmos/azure-cosmos-spark_3/src/test/scala/com/azure/cosmos/spark/NettyCompatibilitySpec.scala new file mode 100644 index 0000000000000..0857b4c3c4027 --- /dev/null +++ b/sdk/cosmos/azure-cosmos-spark_3/src/test/scala/com/azure/cosmos/spark/NettyCompatibilitySpec.scala @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.cosmos.spark + +import com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdLoopNIO +import io.netty.channel.MultiThreadIoEventLoopGroup +import io.netty.util.Version +import io.netty.util.concurrent.DefaultThreadFactory + +import java.util.concurrent.{Callable, TimeUnit} +import scala.collection.JavaConverters._ + +class NettyCompatibilitySpec extends UnitSpec { + private val timeoutInSeconds = 10 + + it should "resolve a consistent Netty 4.2 dependency stack" in { + val versions = Version.identify().values().asScala + .filterNot(_.artifactId().startsWith("netty-tcnative")) + .map(_.artifactVersion()).toSeq + versions should not be empty + all(versions) should startWith("4.2.") + } + + it should "create NIO event loops for the Cosmos transport" in { + val threadName = "cosmos-spark-netty-test" + val eventLoops = new RntbdLoopNIO().newEventLoopGroup(1, new DefaultThreadFactory(threadName)) + try { + eventLoops shouldBe a[MultiThreadIoEventLoopGroup] + eventLoops.next().submit(new Callable[String] { + override def call(): String = Thread.currentThread().getName + }).get(timeoutInSeconds, TimeUnit.SECONDS) should startWith(threadName) + } finally { + eventLoops.shutdownGracefully(0, timeoutInSeconds, TimeUnit.SECONDS).sync() + } + } +} diff --git a/sdk/cosmos/azure-cosmos-spark_4-0_2-13/pom.xml b/sdk/cosmos/azure-cosmos-spark_4-0_2-13/pom.xml index dac2a640c5531..b2673044ade6f 100644 --- a/sdk/cosmos/azure-cosmos-spark_4-0_2-13/pom.xml +++ b/sdk/cosmos/azure-cosmos-spark_4-0_2-13/pom.xml @@ -188,10 +188,86 @@ spark-sql_2.13 4.0.0 + + io.netty + netty-tcnative-boringssl-static + + + io.netty + netty-tcnative-classes + io.netty netty-all + + io.netty + netty-buffer + + + io.netty + netty-codec + + + io.netty + netty-codec-dns + + + io.netty + netty-codec-http + + + io.netty + netty-codec-http2 + + + io.netty + netty-codec-socks + + + io.netty + netty-common + + + io.netty + netty-handler + + + io.netty + netty-transport-native-unix-common + + + io.netty + netty-handler-proxy + + + io.netty + netty-resolver + + + io.netty + netty-resolver-dns + + + io.netty + netty-transport + + + io.netty + netty-transport-classes-epoll + + + io.netty + netty-transport-classes-kqueue + + + io.netty + netty-transport-native-epoll + + + io.netty + netty-transport-native-kqueue + org.slf4j * diff --git a/sdk/cosmos/azure-cosmos-spark_4-1_2-13/pom.xml b/sdk/cosmos/azure-cosmos-spark_4-1_2-13/pom.xml index 1fd6422db2c97..dab39608248dd 100644 --- a/sdk/cosmos/azure-cosmos-spark_4-1_2-13/pom.xml +++ b/sdk/cosmos/azure-cosmos-spark_4-1_2-13/pom.xml @@ -235,10 +235,114 @@ spark-sql_2.13 4.1.0 + + io.netty + netty-tcnative-boringssl-static + + + io.netty + netty-tcnative-classes + io.netty netty-all + + io.netty + netty-buffer + + + io.netty + netty-codec-base + + + io.netty + netty-codec + + + io.netty + netty-codec-dns + + + io.netty + netty-codec-compression + + + io.netty + netty-codec-http + + + io.netty + netty-codec-http2 + + + io.netty + netty-codec-http3 + + + io.netty + netty-codec-socks + + + io.netty + netty-codec-protobuf + + + io.netty + netty-codec-marshalling + + + io.netty + netty-common + + + io.netty + netty-handler + + + io.netty + netty-transport-native-unix-common + + + io.netty + netty-handler-proxy + + + io.netty + netty-resolver + + + io.netty + netty-resolver-dns + + + io.netty + netty-transport + + + io.netty + netty-transport-classes-epoll + + + io.netty + netty-transport-classes-kqueue + + + io.netty + netty-transport-classes-io_uring + + + io.netty + netty-transport-classes-quic + + + io.netty + netty-transport-native-epoll + + + io.netty + netty-transport-native-kqueue + org.slf4j * diff --git a/sdk/cosmos/azure-cosmos-tests/pom.xml b/sdk/cosmos/azure-cosmos-tests/pom.xml index 1153bceb09ace..1e6b49b48bc33 100644 --- a/sdk/cosmos/azure-cosmos-tests/pom.xml +++ b/sdk/cosmos/azure-cosmos-tests/pom.xml @@ -181,7 +181,7 @@ Licensed under the MIT License. io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ConfigsTests.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ConfigsTests.java index f8d0a5c0292bc..ea7ba842d37fe 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ConfigsTests.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/ConfigsTests.java @@ -6,8 +6,14 @@ import com.azure.cosmos.implementation.clienttelemetry.MetricCategory; import com.azure.cosmos.implementation.clienttelemetry.TagName; import com.azure.cosmos.implementation.directconnectivity.Protocol; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.SslContext; +import io.netty.util.ReferenceCountUtil; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import javax.net.ssl.SSLEngine; import java.net.URI; import java.time.Duration; import java.util.EnumSet; @@ -16,6 +22,39 @@ public class ConfigsTests { + @DataProvider(name = "sslValidationSettings") + public Object[][] sslValidationSettings() { + return new Object[][] { + { false, false, false }, { false, false, true }, + { false, true, false }, { false, true, true }, + { true, false, false }, { true, false, true }, + { true, true, false }, { true, true, true } + }; + } + + @Test(groups = { "unit" }, dataProvider = "sslValidationSettings") + public void sslContextPreservesHostnameValidationSettings( + boolean serverCertValidationDisabled, boolean hostnameValidationDisabled, boolean http2Enabled) + throws IllegalAccessException { + + Object original = FieldUtils.readStaticField(Configs.class, "cachedIsHostnameValidationDisabled", true); + SslContext sslContext = null; + SSLEngine engine = null; + try { + FieldUtils.writeStaticField(Configs.class, "cachedIsHostnameValidationDisabled", + hostnameValidationDisabled, true); + sslContext = new Configs().getSslContext(serverCertValidationDisabled, http2Enabled); + engine = sslContext.newEngine(ByteBufAllocator.DEFAULT, "localhost", 443); + + assertThat(engine.getSSLParameters().getEndpointIdentificationAlgorithm()) + .isEqualTo(serverCertValidationDisabled || hostnameValidationDisabled ? null : "HTTPS"); + } finally { + ReferenceCountUtil.release(engine); + ReferenceCountUtil.release(sslContext); + FieldUtils.writeStaticField(Configs.class, "cachedIsHostnameValidationDisabled", original, true); + } + } + @Test(groups = { "unit" }) public void maxHttpHeaderSize() { Configs config = new Configs(); diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/directconnectivity/TcpServerMock/TcpServer.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/directconnectivity/TcpServerMock/TcpServer.java index 3a5ff7605c8db..84deac4399f48 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/directconnectivity/TcpServerMock/TcpServer.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/directconnectivity/TcpServerMock/TcpServer.java @@ -36,7 +36,8 @@ import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.logging.LogLevel; @@ -60,8 +61,8 @@ public class TcpServer { public TcpServer(int port) { this.port = port; - this.parent = new NioEventLoopGroup(); - this.child = new NioEventLoopGroup(); + this.parent = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); + this.child = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); requestManager = new ServerRntbdRequestManager(); } diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopTests.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopTests.java new file mode 100644 index 0000000000000..ecac817f63f71 --- /dev/null +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopTests.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.cosmos.implementation.directconnectivity.rntbd; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.SingleThreadIoEventLoop; +import io.netty.channel.epoll.Epoll; +import io.netty.util.concurrent.DefaultThreadFactory; +import org.testng.SkipException; +import org.testng.annotations.Test; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RntbdLoopTests { + + @Test(groups = { "unit" }) + public void nioChannelRegistration() throws Exception { + RntbdLoop loop = RntbdLoopNativeDetector.getRntbdLoop(false); + assertThat(loop.getName()).isEqualTo("nio"); + verifyChannelRegistration(loop); + } + + @Test(groups = { "unit" }) + public void preferredTransportChannelRegistration() throws Exception { + RntbdLoop loop = RntbdLoopNativeDetector.getRntbdLoop(true); + assertThat(loop.getName()).isEqualTo(Epoll.isAvailable() ? "epoll" : "nio"); + verifyChannelRegistration(loop); + } + + @Test(groups = { "unit" }) + public void epollChannelRegistration() throws Exception { + if (!Epoll.isAvailable()) { + throw new SkipException("Epoll is not available on this platform", Epoll.unavailabilityCause()); + } + verifyChannelRegistration(new RntbdLoopEpoll()); + } + + private static void verifyChannelRegistration(RntbdLoop loop) throws Exception { + String threadName = "rntbd-loop-test-" + loop.getName(); + EventLoopGroup group = loop.newEventLoopGroup(1, new DefaultThreadFactory(threadName)); + Channel channel = null; + try { + assertThat(group).isInstanceOf(MultiThreadIoEventLoopGroup.class); + assertThat(((MultiThreadIoEventLoopGroup) group).executorCount()).isEqualTo(1); + channel = new Bootstrap() + .group(group) + .channel(loop.getChannelClass()) + .handler(new ChannelInboundHandlerAdapter()) + .register().sync().channel(); + + assertThat(channel.isRegistered()).isTrue(); + assertThat(channel).isInstanceOf(loop.getChannelClass()); + assertThat(channel.eventLoop()).isInstanceOf(SingleThreadIoEventLoop.class); + assertThat(RntbdUtils.tryGetExecutorTaskQueueSize(channel.eventLoop())).isGreaterThanOrEqualTo(0); + assertThat(channel.eventLoop().submit(() -> Thread.currentThread().getName()).get(10, TimeUnit.SECONDS)) + .startsWith(threadName); + assertThat(channel.eventLoop().schedule(() -> true, 1, TimeUnit.MILLISECONDS).get(10, TimeUnit.SECONDS)) + .isTrue(); + } finally { + if (channel != null) { + channel.close().sync(); + } + group.shutdownGracefully(0, 5, TimeUnit.SECONDS).sync(); + } + } +} diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/http/ReactorNettyHttpClientTest.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/http/ReactorNettyHttpClientTest.java index 88b85abbdf683..b867c9fed47c8 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/http/ReactorNettyHttpClientTest.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/http/ReactorNettyHttpClientTest.java @@ -9,6 +9,8 @@ import com.azure.cosmos.implementation.Configs; import com.azure.cosmos.implementation.LifeCycleUtils; import com.azure.cosmos.implementation.directconnectivity.ReflectionUtils; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.ChannelOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +48,26 @@ public void after_ReactorNettyHttpClientTest() throws Exception { } } + @Test(groups = "unit") + public void httpClientAllocatorRespectsConfiguration() { + ByteBufAllocator expectedAllocator = System.getProperty("io.netty.allocator.type") == null + ? PooledByteBufAllocator.DEFAULT : ByteBufAllocator.DEFAULT; + reactor.netty.http.client.HttpClient httpClient = + ReflectionUtils.get(reactor.netty.http.client.HttpClient.class, this.reactorNettyHttpClient, "httpClient"); + assertThat(httpClient.configuration().options().get(ChannelOption.ALLOCATOR)) + .isSameAs(expectedAllocator); + + HttpClient unpooledConnectionClient = HttpClient.create(new HttpClientConfig(new Configs())); + try { + httpClient = + ReflectionUtils.get(reactor.netty.http.client.HttpClient.class, unpooledConnectionClient, "httpClient"); + assertThat(httpClient.configuration().options().get(ChannelOption.ALLOCATOR)) + .isSameAs(expectedAllocator); + } finally { + LifeCycleUtils.closeQuietly(unpooledConnectionClient); + } + } + @Test(groups = "unit") public void httpClientWithMaxHeaderSize() { reactor.netty.http.client.HttpClient httpClient = diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/proxy/HttpProxyServer.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/proxy/HttpProxyServer.java index b518db313ee20..6a731f27381b8 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/proxy/HttpProxyServer.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/rx/proxy/HttpProxyServer.java @@ -4,7 +4,8 @@ import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.nio.NioServerSocketChannel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,8 +21,8 @@ public class HttpProxyServer { EventLoopGroup bossGroup; EventLoopGroup workerGroup; public HttpProxyServer() { - bossGroup = new NioEventLoopGroup(1); - workerGroup = new NioEventLoopGroup(); + bossGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); + workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); } public void start() { diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index 9bbaf9c7c67c3..2cab777b4aeef 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -10,8 +10,9 @@ * Fixed the Direct mode (RNTBD) encoding of the `x-ms-cosmos-workload-id` header, which was sent with the wrong token ID (`0x00DC`) and type (`Byte`) instead of `0x00E7` and `UShort`. - See [PR 50673](https://github.com/Azure/azure-sdk-for-java/pull/50673) #### Other Changes + +* Updated Netty to `4.2.19.Final`, Reactor Netty to `1.3.7`, and Reactor Core to `3.8.7`. Migrated direct TCP event loops to the Netty 4.2 I/O handler API while preserving pooled buffers and explicit TLS validation settings. * Added a compact `ppaf` bookmark to each data-plane attempt in `CosmosDiagnostics`, containing the current per-partition write region, failed regions, and the time it was designated, or an empty object when no override is active. -* Upgraded Jackson from `2.18.9` to `2.18.11`. ### 4.83.0 (2026-09-22) diff --git a/sdk/cosmos/azure-cosmos/README.md b/sdk/cosmos/azure-cosmos/README.md index a8bb7e5868bd3..c11a4037373ac 100644 --- a/sdk/cosmos/azure-cosmos/README.md +++ b/sdk/cosmos/azure-cosmos/README.md @@ -95,6 +95,24 @@ A Cosmos DB account contains zero or more databases, a database (DB) contains ze You may read more about databases, containers and items [here](https://learn.microsoft.com/azure/cosmos-db/databases-containers-items). A few important properties defined at the level of the container, among them are provisioned throughput and partition key. +### Direct-mode epoll on the Java module path + +When a named-module application uses Cosmos DB direct mode on JDK 11 or later, the +`com.azure.cosmos` module already requires Netty's `io.netty.transport.classes.epoll` module. +Netty 4.2 places the Linux JNI library in a **separate** explicit module, so including its JAR +on the module path is not enough to make the bundled library visible. On Linux x86_64, +retain or add the matching `io.netty:netty-transport-native-epoll` dependency with the +`linux-x86_64` classifier and add `--add-modules=io.netty.transport.epoll.linux.x86_64` +when launching the application. For another architecture, use its matching classifier +and module name (check with `jar --describe-module --file --release 11`). + +If the native module is not resolved, the bundled JNI library is unavailable to Netty and +Cosmos DB direct mode falls back to JDK NIO unless a native library is supplied another way. +These JPMS launch options do not apply to Java 8 or to classpath applications. Resolving +the module does not by itself prove epoll is usable on the current OS. The +[Netty HTTP plugin's module-path guidance](https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/core/azure-core-http-netty/README.md#native-transports-on-the-module-path) +also covers gateway HTTP and macOS kqueue. + ### Global Distribution - Azure Cosmos DB is a globally distributed database service that's designed to provide low latency, elastic scalability of throughput, well-defined semantics for data consistency, and high availability. In short, if your application needs guaranteed fast response time anywhere in the world, if it's required to be always online, and needs unlimited and elastic scalability of throughput and storage, you should build your application on Azure Cosmos DB. diff --git a/sdk/cosmos/azure-cosmos/pom.xml b/sdk/cosmos/azure-cosmos/pom.xml index bd757939a79d4..983b97ce1e146 100644 --- a/sdk/cosmos/azure-cosmos/pom.xml +++ b/sdk/cosmos/azure-cosmos/pom.xml @@ -67,18 +67,12 @@ Licensed under the MIT License. com.azure azure-core - 1.60.0 + 1.61.0-beta.1 com.azure azure-core-http-netty - 1.16.8 - - - com.azure - azure-core - - + 1.17.0-beta.1 com.fasterxml.jackson.module diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java index bdb83171ee6e1..b152a8f6e3b02 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/Configs.java @@ -500,9 +500,10 @@ private SslContext sslContextInit(boolean serverCertVerificationDisabled, boolea if (serverCertVerificationDisabled) { sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); // disable cert verification - } else if (!isHostnameValidationDisabled()) { - sslContextBuilder.endpointIdentificationAlgorithm("HTTPS"); } + // Netty 4.2 enables hostname validation by default; preserve explicit Cosmos opt-outs. + sslContextBuilder.endpointIdentificationAlgorithm( + serverCertVerificationDisabled || isHostnameValidationDisabled() ? null : "HTTPS"); if (http2Enabled) { sslContextBuilder diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopEpoll.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopEpoll.java index 78f2e2e19bc17..711cfa9471c60 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopEpoll.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopEpoll.java @@ -4,7 +4,8 @@ package com.azure.cosmos.implementation.directconnectivity.rntbd; import io.netty.channel.EventLoopGroup; -import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.epoll.EpollIoHandler; import io.netty.channel.epoll.EpollSocketChannel; import io.netty.channel.socket.SocketChannel; @@ -19,7 +20,7 @@ public String getName() { @Override public EventLoopGroup newEventLoopGroup(int threads, ThreadFactory threadFactory) { - return new EpollEventLoopGroup(threads, threadFactory); + return new MultiThreadIoEventLoopGroup(threads, threadFactory, EpollIoHandler.newFactory()); } @Override diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopNIO.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopNIO.java index 197231a06ea1d..72c24016c44e1 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopNIO.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopNIO.java @@ -4,7 +4,8 @@ package com.azure.cosmos.implementation.directconnectivity.rntbd; import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; @@ -19,7 +20,7 @@ public String getName() { @Override public EventLoopGroup newEventLoopGroup(int threads, ThreadFactory threadFactory) { - return new NioEventLoopGroup(threads, threadFactory); + return new MultiThreadIoEventLoopGroup(threads, threadFactory, NioIoHandler.newFactory()); } @Override diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java index 04db3536834c1..da83532855293 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdServiceEndpoint.java @@ -202,7 +202,7 @@ private Bootstrap getBootStrap(EventLoopGroup eventLoopGroup, Config config) { .option(ChannelOption.ALLOCATOR, config.allocator()) .option(ChannelOption.AUTO_READ, true) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, config.connectTimeoutInMillis()) - .option(ChannelOption.RCVBUF_ALLOCATOR, receiveBufferAllocator) + .option(ChannelOption.RECVBUF_ALLOCATOR, receiveBufferAllocator) .option(ChannelOption.SO_KEEPALIVE, true) .remoteAddress(this.serverKey.getHost(), this.serverKey.getPort()); diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/guava25/hash/Striped64.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/guava25/hash/Striped64.java index df3dff010fb87..972b20ca5a37a 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/guava25/hash/Striped64.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/guava25/hash/Striped64.java @@ -24,8 +24,6 @@ import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; -import static io.netty.util.internal.shaded.org.jctools.util.UnsafeAccess.UNSAFE; - /** * A package-local class holding common representation and mechanics for classes supporting dynamic * striping on 64bit values. The class extends Number so that concrete subclasses must publicly do diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/http/ReactorNettyClient.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/http/ReactorNettyClient.java index f62082edaacc8..674af8a174a92 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/http/ReactorNettyClient.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/implementation/http/ReactorNettyClient.java @@ -6,6 +6,8 @@ import com.azure.cosmos.implementation.Configs; import com.azure.cosmos.implementation.ImplementationBridgeHelpers; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelId; import io.netty.channel.ChannelOption; @@ -16,6 +18,7 @@ import io.netty.resolver.DefaultAddressResolverGroup; import io.netty.util.ReferenceCountUtil; import io.netty.util.ResourceLeakDetector; +import io.netty.util.internal.SystemPropertyUtil; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import org.slf4j.Logger; @@ -51,6 +54,11 @@ private static ImplementationBridgeHelpers.Http2ConnectionConfigHelper.Http2Conn private static final boolean leakDetectionDebuggingEnabled = ResourceLeakDetector.getLevel().ordinal() >= ResourceLeakDetector.Level.ADVANCED.ordinal(); private static final String REACTOR_NETTY_REQUEST_RECORD_KEY = "reactorNettyRequestRecordKey"; + // If io.netty.allocator.type was configured, respect the configuration. Otherwise, fallback to the default setting + // from Netty 4.1 which was the pooled allocator. In the future, this should be validated for if this should use the + // new Netty 4.2 default which is an adaptive allocator. + private static final ByteBufAllocator DEFAULT_ALLOCATOR = SystemPropertyUtil.contains("io.netty.allocator.type") + ? ByteBufAllocator.DEFAULT : PooledByteBufAllocator.DEFAULT; private static final Logger logger = LoggerFactory.getLogger(ReactorNettyClient.class.getSimpleName()); @@ -134,6 +142,7 @@ private void configureChannelPipelineHandlers() { configs.getSslContext( httpClientConfig.isServerCertValidationDisabled(), false))) + .option(ChannelOption.ALLOCATOR, DEFAULT_ALLOCATOR) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) this.httpClientConfig.getConnectionAcquireTimeout().toMillis()) .httpResponseDecoder(httpResponseDecoderSpec -> httpResponseDecoderSpec.maxInitialLineLength(this.httpClientConfig.getMaxInitialLineLength()) diff --git a/sdk/cosmos/azure-cosmos/src/main/java/module-info.java b/sdk/cosmos/azure-cosmos/src/main/java/module-info.java index f3faeda09014b..1b1ac64ec04f3 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/module-info.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/module-info.java @@ -12,6 +12,7 @@ requires io.netty.common; requires io.netty.buffer; requires io.netty.codec; + requires io.netty.codec.compression; requires io.netty.resolver; requires io.netty.codec.http; requires io.netty.codec.http2; diff --git a/sdk/cosmos/cspell.yml b/sdk/cosmos/cspell.yml index 31e8b2522d0bf..a0a1da433f555 100644 --- a/sdk/cosmos/cspell.yml +++ b/sdk/cosmos/cspell.yml @@ -42,6 +42,7 @@ words: - "jgss" - "jnilib" - "jodd" + - "jpms" - "kihn" - "ksql" - "latencyutils" diff --git a/sdk/documentintelligence/azure-ai-documentintelligence/src/test/java/com/azure/ai/documentintelligence/DocumentModelAdministrationAsyncClientTest.java b/sdk/documentintelligence/azure-ai-documentintelligence/src/test/java/com/azure/ai/documentintelligence/DocumentModelAdministrationAsyncClientTest.java index 20ad773830406..c8ab94462887c 100644 --- a/sdk/documentintelligence/azure-ai-documentintelligence/src/test/java/com/azure/ai/documentintelligence/DocumentModelAdministrationAsyncClientTest.java +++ b/sdk/documentintelligence/azure-ai-documentintelligence/src/test/java/com/azure/ai/documentintelligence/DocumentModelAdministrationAsyncClientTest.java @@ -29,7 +29,6 @@ import com.azure.core.util.polling.LongRunningOperationStatus; import com.azure.core.util.polling.PollerFlux; import com.azure.core.util.polling.SyncPoller; -import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -105,7 +104,7 @@ public void deleteModelValidModelIdWithResponse(HttpClient httpClient, syncPoller1.waitForCompletion(); StepVerifier.create(client.deleteModelWithResponse(modelId, null)) - .assertNext(response -> assertEquals(response.getStatusCode(), HttpResponseStatus.NO_CONTENT.code())) + .assertNext(response -> assertEquals(204, response.getStatusCode())) .expectComplete() .verify(DEFAULT_TIMEOUT); } diff --git a/sdk/documentintelligence/azure-ai-documentintelligence/src/test/java/com/azure/ai/documentintelligence/DocumentModelAdministrationClientTest.java b/sdk/documentintelligence/azure-ai-documentintelligence/src/test/java/com/azure/ai/documentintelligence/DocumentModelAdministrationClientTest.java index 28cd4ccdffcaf..a5b1465dcb9e0 100644 --- a/sdk/documentintelligence/azure-ai-documentintelligence/src/test/java/com/azure/ai/documentintelligence/DocumentModelAdministrationClientTest.java +++ b/sdk/documentintelligence/azure-ai-documentintelligence/src/test/java/com/azure/ai/documentintelligence/DocumentModelAdministrationClientTest.java @@ -25,7 +25,6 @@ import com.azure.core.util.BinaryData; import com.azure.core.util.Context; import com.azure.core.util.polling.SyncPoller; -import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; @@ -77,7 +76,7 @@ public void getModelWithResponse(HttpClient httpClient, DocumentIntelligenceServ Response documentModelResponse = client.getModelWithResponse(documentModelDetails.getModelId(), null); - assertEquals(documentModelResponse.getStatusCode(), HttpResponseStatus.OK.code()); + assertEquals(200, documentModelResponse.getStatusCode()); } /** @@ -99,7 +98,7 @@ public void validGetResourceDetailsWithResponse(HttpClient httpClient, DocumentIntelligenceServiceVersion serviceVersion) { client = getModelAdministrationClient(httpClient, serviceVersion); Response resourceDetailsResponse = client.getResourceDetailsWithResponse(null); - assertEquals(resourceDetailsResponse.getStatusCode(), HttpResponseStatus.OK.code()); + assertEquals(200, resourceDetailsResponse.getStatusCode()); } @ParameterizedTest(name = DISPLAY_NAME_WITH_ARGUMENTS) @@ -119,7 +118,7 @@ public void deleteModelValidModelIDWithResponse(HttpClient httpClient, final Response deleteModelWithResponse = client.deleteModelWithResponse(createdModel.getModelId(), null); - assertEquals(deleteModelWithResponse.getStatusCode(), HttpResponseStatus.NO_CONTENT.code()); + assertEquals(204, deleteModelWithResponse.getStatusCode()); } /** diff --git a/sdk/eventhubs/azure-messaging-eventhubs-checkpointstore-jedis/pom.xml b/sdk/eventhubs/azure-messaging-eventhubs-checkpointstore-jedis/pom.xml index 16e5bb6af13ad..e6e4e6c3acef8 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs-checkpointstore-jedis/pom.xml +++ b/sdk/eventhubs/azure-messaging-eventhubs-checkpointstore-jedis/pom.xml @@ -81,7 +81,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/eventhubs/azure-messaging-eventhubs/docs/pom.xml b/sdk/eventhubs/azure-messaging-eventhubs/docs/pom.xml index ee3fc592a1525..a858aa97c1292 100644 --- a/sdk/eventhubs/azure-messaging-eventhubs/docs/pom.xml +++ b/sdk/eventhubs/azure-messaging-eventhubs/docs/pom.xml @@ -25,7 +25,7 @@ io.projectreactor reactor-core - 3.7.19 + 3.8.7 diff --git a/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/FormTrainingAsyncClientTest.java b/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/FormTrainingAsyncClientTest.java index 9aef13b3fe99f..47f66c61b78f0 100644 --- a/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/FormTrainingAsyncClientTest.java +++ b/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/FormTrainingAsyncClientTest.java @@ -21,7 +21,6 @@ import com.azure.core.http.HttpClient; import com.azure.core.util.polling.PollerFlux; import com.azure.core.util.polling.SyncPoller; -import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -35,7 +34,6 @@ import static com.azure.ai.formrecognizer.FormRecognizerClientTestBase.MODEL_ID_NOT_FOUND_ERROR_CODE; import static com.azure.ai.formrecognizer.TestUtils.DISPLAY_NAME_WITH_ARGUMENTS; import static com.azure.ai.formrecognizer.implementation.Utility.toFluxByteBuffer; -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -90,7 +88,7 @@ public void getCustomModelWithResponse(HttpClient httpClient, FormRecognizerServ StepVerifier.create(client.getCustomModelWithResponse(trainedModel.getModelId())) .assertNext(customFormModelResponse -> { - assertEquals(customFormModelResponse.getStatusCode(), HttpResponseStatus.OK.code()); + assertEquals(200, customFormModelResponse.getStatusCode()); validateCustomModelData(syncPoller.getFinalResult(), false, false); }); }); @@ -182,7 +180,7 @@ public void deleteModelValidModelIdWithResponse(HttpClient httpClient, CustomFormModel createdModel = syncPoller.getFinalResult(); StepVerifier.create(client.deleteModelWithResponse(createdModel.getModelId())) - .assertNext(response -> assertEquals(response.getStatusCode(), HttpResponseStatus.NO_CONTENT.code())) + .assertNext(response -> assertEquals(204, response.getStatusCode())) .expectComplete() .verify(DEFAULT_TIMEOUT); @@ -212,7 +210,7 @@ public void deleteModelValidModelIdWithResponseWithoutTrainingLabels(HttpClient CustomFormModel createdModel = syncPoller.getFinalResult(); StepVerifier.create(client.deleteModelWithResponse(createdModel.getModelId())) - .assertNext(response -> assertEquals(response.getStatusCode(), HttpResponseStatus.NO_CONTENT.code())) + .assertNext(response -> assertEquals(204, response.getStatusCode())) .expectComplete() .verify(DEFAULT_TIMEOUT); @@ -671,7 +669,7 @@ public void beginCreateComposedUnlabeledModel(HttpClient httpClient, FormRecogni StepVerifier.create(client.beginCreateComposedModel(modelIdList, new CreateComposedModelOptions()) .setPollInterval(durationTestMode)).thenAwait().expectErrorSatisfies(throwable -> { assertEquals(HttpResponseException.class, throwable.getClass()); - assertEquals(BAD_REQUEST.code(), ((HttpResponseException) throwable).getResponse().getStatusCode()); + assertEquals(400, ((HttpResponseException) throwable).getResponse().getStatusCode()); }).verify(DEFAULT_TIMEOUT); client.deleteModel(model1.getModelId()).block(); @@ -701,7 +699,7 @@ public void beginCreateComposedDuplicateModels(HttpClient httpClient, FormRecogn .setPollInterval(durationTestMode) .getSyncPoller() .waitForCompletion()); - assertEquals(BAD_REQUEST.code(), httpResponseException.getResponse().getStatusCode()); + assertEquals(400, httpResponseException.getResponse().getStatusCode()); client.deleteModel(model1.getModelId()).block(); }); diff --git a/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/FormTrainingClientTest.java b/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/FormTrainingClientTest.java index 36f92011b7faf..854c0cc850ae9 100644 --- a/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/FormTrainingClientTest.java +++ b/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/FormTrainingClientTest.java @@ -23,7 +23,6 @@ import com.azure.core.http.rest.Response; import com.azure.core.util.Context; import com.azure.core.util.polling.SyncPoller; -import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -33,7 +32,6 @@ import static com.azure.ai.formrecognizer.FormRecognizerClientTestBase.MODEL_ID_NOT_FOUND_ERROR_CODE; import static com.azure.ai.formrecognizer.TestUtils.DISPLAY_NAME_WITH_ARGUMENTS; -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -81,7 +79,7 @@ public void getCustomModelWithResponse(HttpClient httpClient, FormRecognizerServ .getFinalResult(); Response customModelWithResponse = client.getCustomModelWithResponse(trainedUnlabeledModel.getModelId(), Context.NONE); - assertEquals(customModelWithResponse.getStatusCode(), HttpResponseStatus.OK.code()); + assertEquals(200, customModelWithResponse.getStatusCode()); validateCustomModelData(customModelWithResponse.getValue(), false, false); }); } @@ -143,7 +141,7 @@ public void validGetAccountPropertiesWithResponse(HttpClient httpClient, // TODO (service bug): APIM error client = getFormTrainingClient(httpClient, serviceVersion); Response accountPropertiesResponse = client.getAccountPropertiesWithResponse(Context.NONE); - assertEquals(accountPropertiesResponse.getStatusCode(), HttpResponseStatus.OK.code()); + assertEquals(200, accountPropertiesResponse.getStatusCode()); validateAccountProperties(accountPropertiesResponse.getValue()); } @@ -161,7 +159,7 @@ public void deleteModelValidModelIdWithResponse(HttpClient httpClient, final Response deleteModelWithResponse = client.deleteModelWithResponse(createdModel.getModelId(), Context.NONE); - assertEquals(deleteModelWithResponse.getStatusCode(), HttpResponseStatus.NO_CONTENT.code()); + assertEquals(204, deleteModelWithResponse.getStatusCode()); final HttpResponseException exception = assertThrows(HttpResponseException.class, () -> client.getCustomModelWithResponse(createdModel.getModelId(), Context.NONE)); final FormRecognizerErrorInformation errorInformation @@ -184,7 +182,7 @@ public void deleteModelValidModelIdWithResponseWithoutTrainingLabels(HttpClient final Response deleteModelWithResponse = client.deleteModelWithResponse(createdModel.getModelId(), Context.NONE); - assertEquals(deleteModelWithResponse.getStatusCode(), HttpResponseStatus.NO_CONTENT.code()); + assertEquals(204, deleteModelWithResponse.getStatusCode()); final HttpResponseException exception = assertThrows(HttpResponseException.class, () -> client.getCustomModelWithResponse(createdModel.getModelId(), Context.NONE)); final FormRecognizerErrorInformation errorInformation @@ -573,7 +571,7 @@ public void beginCreateComposedUnlabeledModel(HttpClient httpClient, FormRecogni final HttpResponseException httpResponseException = assertThrows(HttpResponseException.class, () -> client.beginCreateComposedModel(modelIdList, new CreateComposedModelOptions(), Context.NONE) .setPollInterval(durationTestMode)); - assertEquals(BAD_REQUEST.code(), httpResponseException.getResponse().getStatusCode()); + assertEquals(400, httpResponseException.getResponse().getStatusCode()); client.deleteModel(model1.getModelId()); client.deleteModel(model2.getModelId()); @@ -599,7 +597,7 @@ public void beginCreateComposedDuplicateModels(HttpClient httpClient, FormRecogn () -> client.beginCreateComposedModel(modelIdList, new CreateComposedModelOptions(), Context.NONE) .setPollInterval(durationTestMode) .getFinalResult()); - assertEquals(BAD_REQUEST.code(), httpResponseException.getResponse().getStatusCode()); + assertEquals(400, httpResponseException.getResponse().getStatusCode()); client.deleteModel(model1.getModelId()); }); diff --git a/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/documentanalysis/administration/DocumentModelAdminClientTest.java b/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/documentanalysis/administration/DocumentModelAdminClientTest.java index 01c8fe2412403..54176159d33d2 100644 --- a/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/documentanalysis/administration/DocumentModelAdminClientTest.java +++ b/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/documentanalysis/administration/DocumentModelAdminClientTest.java @@ -29,7 +29,6 @@ import com.azure.core.util.BinaryData; import com.azure.core.util.Context; import com.azure.core.util.polling.SyncPoller; -import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -110,7 +109,7 @@ public void getModelWithResponse(HttpClient httpClient, DocumentAnalysisServiceV = client.getDocumentModelWithResponse(documentModelDetails.getModelId(), Context.NONE); client.deleteDocumentModel(documentModelDetails.getModelId()); - assertEquals(documentModelResponse.getStatusCode(), HttpResponseStatus.OK.code()); + assertEquals(200, documentModelResponse.getStatusCode()); validateDocumentModelData(documentModelResponse.getValue()); }); } @@ -134,7 +133,7 @@ public void validGetResourceDetailsWithResponse(HttpClient httpClient, DocumentAnalysisServiceVersion serviceVersion) { client = getDocumentModelAdministrationClient(httpClient, serviceVersion); Response resourceDetailsResponse = client.getResourceDetailsWithResponse(Context.NONE); - assertEquals(resourceDetailsResponse.getStatusCode(), HttpResponseStatus.OK.code()); + assertEquals(200, resourceDetailsResponse.getStatusCode()); validateResourceInfo(resourceDetailsResponse.getValue()); } @@ -166,7 +165,7 @@ public void deleteModelValidModelIDWithResponse(HttpClient httpClient, final Response deleteModelWithResponse = client.deleteDocumentModelWithResponse(createdModel.getModelId(), Context.NONE); - assertEquals(deleteModelWithResponse.getStatusCode(), HttpResponseStatus.NO_CONTENT.code()); + assertEquals(204, deleteModelWithResponse.getStatusCode()); final HttpResponseException exception = assertThrows(HttpResponseException.class, () -> client.getDocumentModelWithResponse(createdModel.getModelId(), Context.NONE)); final ResponseError responseError = (ResponseError) exception.getValue(); diff --git a/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/documentanalysis/administration/DocumentModelAdministrationAsyncClientTest.java b/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/documentanalysis/administration/DocumentModelAdministrationAsyncClientTest.java index 7c1c164b6c0ed..59961b7503bd7 100644 --- a/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/documentanalysis/administration/DocumentModelAdministrationAsyncClientTest.java +++ b/sdk/formrecognizer/azure-ai-formrecognizer/src/test/java/com/azure/ai/formrecognizer/documentanalysis/administration/DocumentModelAdministrationAsyncClientTest.java @@ -33,7 +33,6 @@ import com.azure.core.util.polling.PollerFlux; import com.azure.core.util.polling.SyncPoller; import com.azure.identity.AzureAuthorityHosts; -import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; @@ -134,7 +133,7 @@ public void deleteModelValidModelIdWithResponse(HttpClient httpClient, DocumentModelDetails createdModel = syncPoller1.getFinalResult(); StepVerifier.create(client.deleteDocumentModelWithResponse(createdModel.getModelId())) - .assertNext(response -> assertEquals(response.getStatusCode(), HttpResponseStatus.NO_CONTENT.code())) + .assertNext(response -> assertEquals(204, response.getStatusCode())) .expectComplete() .verify(DEFAULT_TIMEOUT); @@ -482,7 +481,7 @@ public void getModelWithResponse(HttpClient httpClient, DocumentAnalysisServiceV StepVerifier.create(client.getDocumentModelWithResponse(createdModel.getModelId())) .assertNext(documentModelResponse -> { - assertEquals(documentModelResponse.getStatusCode(), HttpResponseStatus.OK.code()); + assertEquals(200, documentModelResponse.getStatusCode()); validateDocumentModelData(documentModelResponse.getValue()); }); // TODO (alzimmer): This test needs to be recorded again as it was never verifying, therefore never diff --git a/sdk/identity/azure-identity/pom.xml b/sdk/identity/azure-identity/pom.xml index 6a23070d50ff4..4b7c7e2a5b716 100644 --- a/sdk/identity/azure-identity/pom.xml +++ b/sdk/identity/azure-identity/pom.xml @@ -18,6 +18,7 @@ --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-exports com.azure.core/com.azure.core.implementation.util=ALL-UNNAMED + --add-reads com.azure.identity=io.netty.transport,io.netty.buffer,io.netty.handler,io.netty.codec.http,io.netty.common false @@ -35,13 +36,13 @@ com.azure azure-core - 1.60.0 + 1.61.0-beta.1 com.azure azure-core-http-netty - 1.16.8 + 1.17.0-beta.1 @@ -62,7 +63,7 @@ com.azure azure-core-test - 1.27.0-beta.19 + 1.27.0-beta.20 test diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialIdentityBindingTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialIdentityBindingTest.java index ab8a2ec756ff1..ba206acdf5d30 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialIdentityBindingTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/WorkloadIdentityCredentialIdentityBindingTest.java @@ -15,8 +15,9 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; +import io.netty.channel.MultiThreadIoEventLoopGroup; import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.DefaultFullHttpResponse; @@ -76,8 +77,8 @@ public class WorkloadIdentityCredentialIdentityBindingTest { @TempDir Path tempDir; - private NioEventLoopGroup bossGroup; - private NioEventLoopGroup workerGroup; + private MultiThreadIoEventLoopGroup bossGroup; + private MultiThreadIoEventLoopGroup workerGroup; private Channel serverChannel; private String serverBaseUrl; private Path tokenFilePath; @@ -543,8 +544,8 @@ private void startNettyHttpsServer(PrivateKey privateKey, X509Certificate certif SslContext sslContext = SslContextBuilder.forServer(privateKey, certificate).sslProvider(SslProvider.OPENSSL).build(); - bossGroup = new NioEventLoopGroup(1); - workerGroup = new NioEventLoopGroup(); + bossGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); + workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTest.java b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTest.java index 111e05ecb8bfd..374df5aa02b31 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTest.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTest.java @@ -15,7 +15,6 @@ import com.azure.security.keyvault.secrets.models.DeletedSecret; import com.azure.security.keyvault.secrets.models.KeyVaultSecret; import com.azure.security.keyvault.secrets.models.SecretProperties; -import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -230,8 +229,7 @@ public void deleteSecret(HttpClient httpClient, SecretServiceVersion serviceVers public void deleteSecretNotFound(HttpClient httpClient, SecretServiceVersion serviceVersion) { createClient(httpClient, serviceVersion); - assertRestException(() -> secretClient.beginDeleteSecret("non-existing"), ResourceNotFoundException.class, - HttpResponseStatus.NOT_FOUND.code()); + assertRestException(() -> secretClient.beginDeleteSecret("non-existing"), ResourceNotFoundException.class, 404); } /** @@ -309,7 +307,7 @@ public void recoverDeletedSecretNotFound(HttpClient httpClient, SecretServiceVer createClient(httpClient, serviceVersion); assertRestException(() -> secretClient.beginRecoverDeletedSecret("non-existing"), - ResourceNotFoundException.class, HttpResponseStatus.NOT_FOUND.code()); + ResourceNotFoundException.class, 404); } /** diff --git a/sdk/metricsadvisor/azure-ai-metricsadvisor/src/test/java/com/azure/ai/metricsadvisor/DataFeedAsyncClientTest.java b/sdk/metricsadvisor/azure-ai-metricsadvisor/src/test/java/com/azure/ai/metricsadvisor/DataFeedAsyncClientTest.java index e809843003ff0..dcc8660bd6c6e 100644 --- a/sdk/metricsadvisor/azure-ai-metricsadvisor/src/test/java/com/azure/ai/metricsadvisor/DataFeedAsyncClientTest.java +++ b/sdk/metricsadvisor/azure-ai-metricsadvisor/src/test/java/com/azure/ai/metricsadvisor/DataFeedAsyncClientTest.java @@ -16,7 +16,6 @@ import com.azure.core.test.annotation.DoNotRecord; import com.azure.core.test.http.MockHttpResponse; import com.azure.core.util.CoreUtils; -import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -346,7 +345,7 @@ public void getDataFeedValidId(HttpClient httpClient, MetricsAdvisorServiceVersi // Act & Assert StepVerifier.create(client.getDataFeedWithResponse(createdDataFeed.getId())) .assertNext(dataFeedResponse -> { - assertEquals(dataFeedResponse.getStatusCode(), HttpResponseStatus.OK.code()); + assertEquals(200, dataFeedResponse.getStatusCode()); validateDataFeedResult(createdDataFeed, dataFeedResponse.getValue(), SQL_SERVER_DB); }); // TODO (alzimmer): This test needs to be recorded again as it was never verifying, therefore never @@ -725,7 +724,7 @@ public void deleteDataFeedIdWithResponse(HttpClient httpClient, MetricsAdvisorSe assertNotNull(createdDataFeed); StepVerifier.create(client.deleteDataFeedWithResponse(createdDataFeed.getId())) - .assertNext(response -> assertEquals(response.getStatusCode(), HttpResponseStatus.NO_CONTENT.code())) + .assertNext(response -> assertEquals(204, response.getStatusCode())) .expectComplete() .verify(DEFAULT_TIMEOUT); diff --git a/sdk/metricsadvisor/azure-ai-metricsadvisor/src/test/java/com/azure/ai/metricsadvisor/DataFeedClientTest.java b/sdk/metricsadvisor/azure-ai-metricsadvisor/src/test/java/com/azure/ai/metricsadvisor/DataFeedClientTest.java index c6f1ec9b5b355..a81dcc4edf2ed 100644 --- a/sdk/metricsadvisor/azure-ai-metricsadvisor/src/test/java/com/azure/ai/metricsadvisor/DataFeedClientTest.java +++ b/sdk/metricsadvisor/azure-ai-metricsadvisor/src/test/java/com/azure/ai/metricsadvisor/DataFeedClientTest.java @@ -23,7 +23,6 @@ import com.azure.core.test.http.MockHttpResponse; import com.azure.core.util.Context; import com.azure.core.util.CoreUtils; -import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -316,7 +315,7 @@ public void getDataFeedValidId(HttpClient httpClient, MetricsAdvisorServiceVersi // Act & Assert final Response dataFeedResponse = client.getDataFeedWithResponse(createdDataFeed.getId(), Context.NONE); - assertEquals(dataFeedResponse.getStatusCode(), HttpResponseStatus.OK.code()); + assertEquals(200, dataFeedResponse.getStatusCode()); validateDataFeedResult(createdDataFeed, dataFeedResponse.getValue(), SQL_SERVER_DB); }, SQL_SERVER_DB); } finally { @@ -706,8 +705,7 @@ public void deleteDataFeedIdWithResponse(HttpClient httpClient, MetricsAdvisorSe creatDataFeedRunner(dataFeed -> { final DataFeed createdDataFeed = client.createDataFeed(dataFeed); - assertEquals(HttpResponseStatus.NO_CONTENT.code(), - client.deleteDataFeedWithResponse(createdDataFeed.getId(), Context.NONE).getStatusCode()); + assertEquals(204, client.deleteDataFeedWithResponse(createdDataFeed.getId(), Context.NONE).getStatusCode()); // Act & Assert MetricsAdvisorResponseException exception = assertThrows(MetricsAdvisorResponseException.class, diff --git a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/pom.xml b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/pom.xml index 0d1c4bc1e9605..21acaf9209663 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/pom.xml +++ b/sdk/monitor/azure-monitor-opentelemetry-autoconfigure/pom.xml @@ -38,6 +38,7 @@ --add-opens com.azure.monitor.opentelemetry.autoconfigure/com.azure.monitor.opentelemetry.autoconfigure=ALL-UNNAMED --add-exports com.azure.core/com.azure.core.implementation.util=ALL-UNNAMED --add-reads com.azure.monitor.opentelemetry.autoconfigure=com.azure.core.tracing.opentelemetry + --add-reads com.azure.monitor.opentelemetry.autoconfigure=io.netty.transport false diff --git a/sdk/openai/azure-ai-openai-realtime/CHANGELOG.md b/sdk/openai/azure-ai-openai-realtime/CHANGELOG.md index b64e6ac33abd5..29ba79c4bccd4 100644 --- a/sdk/openai/azure-ai-openai-realtime/CHANGELOG.md +++ b/sdk/openai/azure-ai-openai-realtime/CHANGELOG.md @@ -2,6 +2,8 @@ #### Dependency Updates +- Migrated `netty-codec-http` to `4.2.19.Final` and updated WebSocket event loops for Netty 4.2. + ## 1.0.0-beta.1 (TBD) -- Azure OpenAI Realtime client library for Java. \ No newline at end of file +- Azure OpenAI Realtime client library for Java. diff --git a/sdk/openai/azure-ai-openai-realtime/pom.xml b/sdk/openai/azure-ai-openai-realtime/pom.xml index a666b0f016daf..77fbbc6d14ea9 100644 --- a/sdk/openai/azure-ai-openai-realtime/pom.xml +++ b/sdk/openai/azure-ai-openai-realtime/pom.xml @@ -59,14 +59,14 @@ com.azure azure-core - 1.60.0 + 1.61.0-beta.1 io.netty netty-codec-http - 4.1.138.Final + 4.2.19.Final @@ -81,7 +81,13 @@ com.azure azure-core-test - 1.27.0-beta.19 + 1.27.0-beta.20 + test + + + com.azure + azure-core-http-netty + 1.17.0-beta.1 test @@ -102,7 +108,7 @@ - io.netty:netty-codec-http:[4.1.138.Final] + io.netty:netty-codec-http:[4.2.19.Final] diff --git a/sdk/openai/azure-ai-openai-realtime/src/main/java/com/azure/ai/openai/realtime/implementation/websocket/WebSocketSessionNettyImpl.java b/sdk/openai/azure-ai-openai-realtime/src/main/java/com/azure/ai/openai/realtime/implementation/websocket/WebSocketSessionNettyImpl.java index a033e6c6cac74..9bb3205ef776f 100644 --- a/sdk/openai/azure-ai-openai-realtime/src/main/java/com/azure/ai/openai/realtime/implementation/websocket/WebSocketSessionNettyImpl.java +++ b/sdk/openai/azure-ai-openai-realtime/src/main/java/com/azure/ai/openai/realtime/implementation/websocket/WebSocketSessionNettyImpl.java @@ -14,7 +14,8 @@ import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.HttpClientCodec; @@ -34,7 +35,6 @@ import javax.net.ssl.SSLException; import java.net.URI; -import java.net.URISyntaxException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -103,7 +103,7 @@ protected void initChannel(SocketChannel ch) { this.closeHandler = closeHandler; } - void connect() throws URISyntaxException, SSLException, InterruptedException, ExecutionException { + void connect() throws SSLException, InterruptedException, ExecutionException { String scheme = uri.getScheme() == null ? "ws" : uri.getScheme(); final String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost(); final int port; @@ -131,7 +131,7 @@ void connect() throws URISyntaxException, SSLException, InterruptedException, Ex sslCtx = null; } - group = new NioEventLoopGroup(); + group = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); handshaker = WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, this.subProtocol, true, this.headers); diff --git a/sdk/openai/azure-ai-openai-realtime/src/main/java/module-info.java b/sdk/openai/azure-ai-openai-realtime/src/main/java/module-info.java index 6ea952fb0ced4..e53704ff8e5e8 100644 --- a/sdk/openai/azure-ai-openai-realtime/src/main/java/module-info.java +++ b/sdk/openai/azure-ai-openai-realtime/src/main/java/module-info.java @@ -10,6 +10,7 @@ requires io.netty.transport; requires io.netty.handler; requires io.netty.codec; + requires io.netty.codec.compression; requires io.netty.codec.http; exports com.azure.ai.openai.realtime; diff --git a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/administration/ServiceBusAdministrationAsyncClientTest.java b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/administration/ServiceBusAdministrationAsyncClientTest.java index 8e607a15a11af..206cc29a186cd 100644 --- a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/administration/ServiceBusAdministrationAsyncClientTest.java +++ b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/administration/ServiceBusAdministrationAsyncClientTest.java @@ -38,7 +38,6 @@ import com.azure.messaging.servicebus.administration.models.QueueRuntimeProperties; import com.azure.xml.XmlSerializable; import com.azure.xml.XmlWriter; -import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -528,7 +527,7 @@ void deleteRuleWithResponse() { // Act & Assert StepVerifier.create(client.deleteRuleWithResponse(topicName, subscriptionName, ruleName)) - .assertNext(actual -> assertEquals(actual.getStatusCode(), HttpResponseStatus.NO_CONTENT.code())) + .assertNext(actual -> assertEquals(204, actual.getStatusCode())) .expectComplete() .verify(DEFAULT_TIMEOUT); } @@ -555,7 +554,7 @@ void deleteSubscriptionWithResponse() { // Act & Assert StepVerifier.create(client.deleteSubscriptionWithResponse(topicName, subscriptionName)) - .assertNext(actual -> assertEquals(actual.getStatusCode(), HttpResponseStatus.NO_CONTENT.code())) + .assertNext(actual -> assertEquals(204, actual.getStatusCode())) .expectComplete() .verify(DEFAULT_TIMEOUT); } @@ -581,7 +580,7 @@ void deleteTopicWithResponse() { // Act & Assert StepVerifier.create(client.deleteTopicWithResponse(topicName)) - .assertNext(actual -> assertEquals(actual.getStatusCode(), HttpResponseStatus.NO_CONTENT.code())) + .assertNext(actual -> assertEquals(204, actual.getStatusCode())) .expectComplete() .verify(DEFAULT_TIMEOUT); } diff --git a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/administration/ServiceBusAdministrationClientTest.java b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/administration/ServiceBusAdministrationClientTest.java index 9d8174d3acc22..c5c780ed6db3e 100644 --- a/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/administration/ServiceBusAdministrationClientTest.java +++ b/sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/administration/ServiceBusAdministrationClientTest.java @@ -36,7 +36,6 @@ import com.azure.messaging.servicebus.administration.models.QueueProperties; import com.azure.messaging.servicebus.administration.models.QueueRuntimeProperties; import com.azure.messaging.servicebus.administration.models.SubscriptionProperties; -import io.netty.handler.codec.http.HttpResponseStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -508,7 +507,7 @@ void deleteRuleWithResponse() { final Response actual = client.deleteRuleWithResponse(topicName, subscriptionName, ruleName, context); // Assert - assertEquals(actual.getStatusCode(), HttpResponseStatus.NO_CONTENT.code()); + assertEquals(204, actual.getStatusCode()); } @Test @@ -534,7 +533,7 @@ void deleteSubscriptionWithResponse() { final Response actual = client.deleteSubscriptionWithResponse(topicName, subscriptionName, context); // Assert - assertEquals(actual.getStatusCode(), HttpResponseStatus.NO_CONTENT.code()); + assertEquals(204, actual.getStatusCode()); } @Test @@ -560,7 +559,7 @@ void deleteTopicWithResponse() { final Response actual = client.deleteTopicWithResponse(topicName, context); // Assert - assertEquals(actual.getStatusCode(), HttpResponseStatus.NO_CONTENT.code()); + assertEquals(204, actual.getStatusCode()); } /** diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md b/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md index 5114a28e73ae7..8c3881a6746dc 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md @@ -10,6 +10,8 @@ ### Other Changes +- Aligned Reactor version resolution and test HTTP transport dependencies with Netty 4.2, and added offline transport compatibility tests. + ## 7.4.0 (2026-07-24) Please refer to [spring/CHANGELOG.md](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/CHANGELOG.md#740-2026-07-24) for more details. @@ -253,4 +255,3 @@ Please refer to [spring/CHANGELOG.md](https://github.com/Azure/azure-sdk-for-jav ## 4.0.0-beta.2 (2021-11-22) Please refer to [spring/CHANGELOG.md](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/CHANGELOG.md#400-beta2-2021-11-22) for more details. - diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml index 2c1dc467d8885..48e5a0dd36102 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml +++ b/sdk/spring/spring-cloud-azure-autoconfigure/pom.xml @@ -102,6 +102,12 @@ lettuce-core 7.5.2.RELEASE true + + + io.projectreactor + reactor-core + + org.springframework.boot @@ -193,7 +199,7 @@ com.azure azure-core - 1.60.0 + 1.61.0-beta.1 true @@ -417,6 +423,13 @@ + + + com.azure + azure-core-http-netty + 1.17.0-beta.1 + test + org.springframework.boot spring-boot-starter-test diff --git a/sdk/spring/spring-cloud-azure-starter-servicebus-jms/pom.xml b/sdk/spring/spring-cloud-azure-starter-servicebus-jms/pom.xml index 1b31055bb9cf4..93557e043243e 100644 --- a/sdk/spring/spring-cloud-azure-starter-servicebus-jms/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-servicebus-jms/pom.xml @@ -106,7 +106,7 @@ currently released version and a lower version is resolved. --> io.netty netty-buffer - 4.2.17.Final + 4.2.19.Final io.netty diff --git a/sdk/voicelive/azure-ai-voicelive/pom.xml b/sdk/voicelive/azure-ai-voicelive/pom.xml index 2957a0dac503e..4e816b571c224 100644 --- a/sdk/voicelive/azure-ai-voicelive/pom.xml +++ b/sdk/voicelive/azure-ai-voicelive/pom.xml @@ -84,7 +84,7 @@ Code generated by Microsoft (R) TypeSpec Code Generator. io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/webpubsub/azure-messaging-webpubsub-chat/pom.xml b/sdk/webpubsub/azure-messaging-webpubsub-chat/pom.xml index f95a74d16a943..884bafaebba30 100644 --- a/sdk/webpubsub/azure-messaging-webpubsub-chat/pom.xml +++ b/sdk/webpubsub/azure-messaging-webpubsub-chat/pom.xml @@ -48,12 +48,12 @@ com.azure azure-core - 1.60.0 + 1.61.0-beta.1 com.azure azure-core-http-netty - 1.16.8 + 1.17.0-beta.1 com.nimbusds @@ -63,7 +63,7 @@ com.azure azure-core-test - 1.27.0-beta.19 + 1.27.0-beta.20 test @@ -75,7 +75,7 @@ io.projectreactor.netty reactor-netty-http - 1.2.18 + 1.3.7 test @@ -90,7 +90,7 @@ com.nimbusds:nimbus-jose-jwt:[9.37.3] - io.projectreactor.netty:reactor-netty-http:[1.2.18] + io.projectreactor.netty:reactor-netty-http:[1.3.7] diff --git a/sdk/webpubsub/azure-messaging-webpubsub-client/CHANGELOG.md b/sdk/webpubsub/azure-messaging-webpubsub-client/CHANGELOG.md index 5f0708972dc2d..cdb638dfd811e 100644 --- a/sdk/webpubsub/azure-messaging-webpubsub-client/CHANGELOG.md +++ b/sdk/webpubsub/azure-messaging-webpubsub-client/CHANGELOG.md @@ -14,7 +14,7 @@ #### Dependency Updates -- Upgraded `netty-codec-http` from `4.1.137.Final` to version `4.1.138.Final`. +- Migrated `netty-codec-http` to `4.2.19.Final` and updated WebSocket event loops for Netty 4.2. ## 1.1.11 (2026-10-06) diff --git a/sdk/webpubsub/azure-messaging-webpubsub-client/pom.xml b/sdk/webpubsub/azure-messaging-webpubsub-client/pom.xml index b5c069ce771a1..d4235659fa57a 100644 --- a/sdk/webpubsub/azure-messaging-webpubsub-client/pom.xml +++ b/sdk/webpubsub/azure-messaging-webpubsub-client/pom.xml @@ -48,21 +48,27 @@ com.azure azure-core - 1.60.0 + 1.61.0-beta.1 io.netty netty-codec-http - 4.1.138.Final + 4.2.19.Final com.azure azure-core-test - 1.27.0-beta.19 + 1.27.0-beta.20 + test + + + com.azure + azure-core-http-netty + 1.17.0-beta.1 test @@ -101,7 +107,7 @@ - io.netty:netty-codec-http:[4.1.138.Final] + io.netty:netty-codec-http:[4.2.19.Final] diff --git a/sdk/webpubsub/azure-messaging-webpubsub-client/src/main/java/com/azure/messaging/webpubsub/client/implementation/websocket/WebSocketSessionNettyImpl.java b/sdk/webpubsub/azure-messaging-webpubsub-client/src/main/java/com/azure/messaging/webpubsub/client/implementation/websocket/WebSocketSessionNettyImpl.java index 8229e60f16cf8..9f46a915ee102 100644 --- a/sdk/webpubsub/azure-messaging-webpubsub-client/src/main/java/com/azure/messaging/webpubsub/client/implementation/websocket/WebSocketSessionNettyImpl.java +++ b/sdk/webpubsub/azure-messaging-webpubsub-client/src/main/java/com/azure/messaging/webpubsub/client/implementation/websocket/WebSocketSessionNettyImpl.java @@ -10,11 +10,15 @@ import com.azure.messaging.webpubsub.client.implementation.models.WebPubSubMessage; import com.azure.messaging.webpubsub.client.models.ConnectFailedException; import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.DefaultHttpHeaders; @@ -29,6 +33,7 @@ import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; +import io.netty.util.internal.SystemPropertyUtil; import javax.net.ssl.SSLException; import java.net.URI; @@ -75,7 +80,7 @@ protected void initChannel(SocketChannel ch) { p.addLast(sslCtx.newHandler(ch.alloc(), host, port)); } - p.addLast(new HttpClientCodec(), new HttpObjectAggregator(8192), WebSocketClientCompressionHandler.INSTANCE, + p.addLast(new HttpClientCodec(), new HttpObjectAggregator(8192), new WebSocketClientCompressionHandler(0), handler); } } @@ -123,7 +128,7 @@ void connect() throws URISyntaxException, SSLException, InterruptedException, Ex sslCtx = null; } - group = new NioEventLoopGroup(); + group = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); handshaker = WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, protocol, true, new DefaultHttpHeaders().add(HttpHeaderName.USER_AGENT.getCaseInsensitiveName(), userAgent)); @@ -133,6 +138,13 @@ void connect() throws URISyntaxException, SSLException, InterruptedException, Ex Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) + .option(ChannelOption.ALLOCATOR, + // If io.netty.allocator.type was configured, respect the configuration. Otherwise, fallback to the + // default setting from Netty 4.1 which was the pooled allocator. In the future, this should be + // validated for if this should use the new Netty 4.2 default which is an adaptive allocator. + SystemPropertyUtil.contains("io.netty.allocator.type") + ? ByteBufAllocator.DEFAULT + : PooledByteBufAllocator.DEFAULT) .handler(new WebSocketChannelHandler(host, port, sslCtx, clientHandler)); final CompletableFuture handshakeCallbackFuture = new CompletableFuture<>(); diff --git a/sdk/webpubsub/azure-messaging-webpubsub-client/src/main/java/module-info.java b/sdk/webpubsub/azure-messaging-webpubsub-client/src/main/java/module-info.java index b701da2f86b2b..478c4b9eb9a9d 100644 --- a/sdk/webpubsub/azure-messaging-webpubsub-client/src/main/java/module-info.java +++ b/sdk/webpubsub/azure-messaging-webpubsub-client/src/main/java/module-info.java @@ -9,6 +9,7 @@ requires io.netty.transport; requires io.netty.handler; requires io.netty.codec; + requires io.netty.codec.compression; requires io.netty.codec.http; exports com.azure.messaging.webpubsub.client;