From b8c07eb8b5ea52db2dfb570447ceb156b07e697e Mon Sep 17 00:00:00 2001 From: alzimmermsft <48699787+alzimmermsft@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:50:21 -0400 Subject: [PATCH] Upgrade Netty and Reactor versions --- common/perf-test-core/pom.xml | 2 +- eng/versioning/external_dependencies.txt | 34 +-- ...upported_external_dependency_versions.json | 8 +- sdk/clientcore/http-netty4/CHANGELOG.md | 4 + sdk/clientcore/http-netty4/pom.xml | 62 ++-- .../http/netty4/NettyHttpClientBuilder.java | 105 ++++--- .../netty4/implementation/Netty4Utility.java | 10 +- .../src/main/java/module-info.java | 1 + .../netty4/NettyHttp2HttpClientTests.java | 36 +++ .../netty4/NettyHttpClientBuilderTests.java | 269 +++++++++--------- .../Netty4ConnectionPoolTests.java | 5 +- .../implementation/Netty4UtilityTests.java | 28 ++ .../azure-communication-common/pom.xml | 2 +- sdk/core/azure-core-experimental/pom.xml | 2 +- .../azure-core-http-jdk-httpclient/pom.xml | 2 +- sdk/core/azure-core-http-netty/CHANGELOG.md | 8 +- sdk/core/azure-core-http-netty/README.md | 13 +- sdk/core/azure-core-http-netty/pom.xml | 50 ++-- .../netty/NettyAsyncHttpClientBuilder.java | 17 +- .../ByteBufWriteSubscriber.java | 38 ++- .../NettyAsyncHttpResponse.java | 6 +- .../netty/implementation/NettyUtility.java | 5 +- .../src/main/java/module-info.java | 3 + .../native-image.properties | 6 - ...cHttpClientBuilderJavaDocCodeSnippets.java | 7 +- .../NettyAsyncHttpClientBuilderTests.java | 51 +++- .../ByteBufWriterSubscriberTests.java | 43 +++ .../implementation/NettyUtilityTests.java | 4 + sdk/core/azure-core-http-okhttp/pom.xml | 2 +- sdk/core/azure-core-http-vertx/README.md | 5 +- sdk/core/azure-core-http-vertx/pom.xml | 2 +- sdk/core/azure-core-management/pom.xml | 2 +- sdk/core/azure-core-perf/pom.xml | 2 +- .../azure-core-serializer-avro-apache/pom.xml | 2 +- .../azure-core-serializer-json-gson/pom.xml | 2 +- .../pom.xml | 2 +- sdk/core/azure-core-test/CHANGELOG.md | 2 + sdk/core/azure-core-test/README.md | 1 - sdk/core/azure-core-test/pom.xml | 4 +- .../azure-core-tracing-opentelemetry/pom.xml | 2 +- sdk/core/azure-core-version-tests/pom.xml | 4 +- sdk/core/azure-core/CHANGELOG.md | 4 + sdk/core/azure-core/pom.xml | 6 +- sdk/cosmos/azure-cosmos-encryption/pom.xml | 2 +- .../pom.xml | 2 +- sdk/cosmos/azure-cosmos-spark_3/pom.xml | 2 +- sdk/cosmos/azure-cosmos-tests/pom.xml | 2 +- .../cosmos/implementation/ConfigsTests.java | 39 +++ .../TcpServerMock/TcpServer.java | 7 +- .../rntbd/RntbdLoopTests.java | 73 +++++ .../http/ReactorNettyHttpClientTest.java | 22 ++ .../cosmos/rx/proxy/HttpProxyServer.java | 7 +- sdk/cosmos/azure-cosmos/CHANGELOG.md | 1 + sdk/cosmos/azure-cosmos/pom.xml | 4 +- .../azure/cosmos/implementation/Configs.java | 5 +- .../rntbd/RntbdLoopEpoll.java | 5 +- .../rntbd/RntbdLoopNIO.java | 5 +- .../rntbd/RntbdServiceEndpoint.java | 2 +- .../guava25/hash/Striped64.java | 2 - .../http/ReactorNettyClient.java | 6 + .../src/main/java/module-info.java | 1 + sdk/e2e/pom.xml | 2 +- .../pom.xml | 2 +- .../azure-messaging-eventhubs/docs/pom.xml | 2 +- .../azure-ai-openai-realtime/CHANGELOG.md | 2 + sdk/openai/azure-ai-openai-realtime/pom.xml | 8 +- .../websocket/WebSocketSessionNettyImpl.java | 5 +- .../src/main/java/module-info.java | 1 + sdk/voicelive/azure-ai-voicelive/pom.xml | 2 +- .../CHANGELOG.md | 2 +- .../azure-messaging-webpubsub-client/pom.xml | 8 +- .../websocket/WebSocketSessionNettyImpl.java | 15 +- .../src/main/java/module-info.java | 1 + 73 files changed, 737 insertions(+), 363 deletions(-) create mode 100644 sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/implementation/directconnectivity/rntbd/RntbdLoopTests.java diff --git a/common/perf-test-core/pom.xml b/common/perf-test-core/pom.xml index d2ffe784303e..02f9205c79dd 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/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index c3829f0d3b30..4d31a42cfe08 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -44,22 +44,22 @@ 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.137.Final -io.netty:netty-common;4.1.137.Final -io.netty:netty-codec;4.1.137.Final -io.netty:netty-codec-http;4.1.137.Final -io.netty:netty-codec-http2;4.1.137.Final -io.netty:netty-handler;4.1.137.Final -io.netty:netty-handler-proxy;4.1.137.Final -io.netty:netty-resolver;4.1.137.Final -io.netty:netty-resolver-dns;4.1.137.Final +io.netty:netty-buffer;4.2.17.Final +io.netty:netty-common;4.2.17.Final +io.netty:netty-codec-base;4.2.17.Final +io.netty:netty-codec-http;4.2.17.Final +io.netty:netty-codec-http2;4.2.17.Final +io.netty:netty-handler;4.2.17.Final +io.netty:netty-handler-proxy;4.2.17.Final +io.netty:netty-resolver;4.2.17.Final +io.netty:netty-resolver-dns;4.2.17.Final io.netty:netty-tcnative-boringssl-static;2.0.81.Final -io.netty:netty-transport;4.1.137.Final -io.netty:netty-transport-native-epoll;4.1.137.Final -io.netty:netty-transport-native-unix-common;4.1.137.Final -io.netty:netty-transport-native-kqueue;4.1.137.Final -io.projectreactor.netty:reactor-netty-http;1.2.18 -io.projectreactor:reactor-core;3.7.19 +io.netty:netty-transport;4.2.17.Final +io.netty:netty-transport-native-epoll;4.2.17.Final +io.netty:netty-transport-native-unix-common;4.2.17.Final +io.netty:netty-transport-native-kqueue;4.2.17.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 @@ -125,7 +125,7 @@ io.opentelemetry:opentelemetry-bom;1.58.0 io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8;2.24.0-alpha io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter;2.24.0 io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0;2.24.0-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 @@ -207,7 +207,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 35fddef80f79..722d67abfd9e 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/sdk/clientcore/http-netty4/CHANGELOG.md b/sdk/clientcore/http-netty4/CHANGELOG.md index 13dd08af78ab..ca7dcbf1dc15 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.17.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/pom.xml b/sdk/clientcore/http-netty4/pom.xml index 9c02f031baea..77dd38f833d5 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.137.Final + 4.2.17.Final @@ -69,52 +69,52 @@ io.netty netty-buffer - 4.1.137.Final + 4.2.17.Final io.netty - netty-codec - 4.1.137.Final + netty-codec-base + 4.2.17.Final io.netty netty-codec-http - 4.1.137.Final + 4.2.17.Final io.netty netty-codec-http2 - 4.1.137.Final + 4.2.17.Final io.netty netty-common - 4.1.137.Final + 4.2.17.Final io.netty netty-handler - 4.1.137.Final + 4.2.17.Final io.netty netty-handler-proxy - 4.1.137.Final + 4.2.17.Final io.netty netty-resolver - 4.1.137.Final + 4.2.17.Final io.netty netty-resolver-dns - 4.1.137.Final + 4.2.17.Final io.netty netty-transport - 4.1.137.Final + 4.2.17.Final @@ -126,32 +126,18 @@ test - - io.netty - netty-transport-native-unix-common - 4.1.137.Final - linux-x86_64 - test - io.netty netty-transport-native-epoll - 4.1.137.Final + 4.2.17.Final linux-x86_64 test - - io.netty - netty-transport-native-unix-common - 4.1.137.Final - osx-x86_64 - test - io.netty netty-transport-native-kqueue - 4.1.137.Final + 4.2.17.Final osx-x86_64 test @@ -223,16 +209,16 @@ - io.netty:netty-buffer:[4.1.137.Final] - io.netty:netty-codec:[4.1.137.Final] - io.netty:netty-codec-http:[4.1.137.Final] - io.netty:netty-codec-http2:[4.1.137.Final] - io.netty:netty-common:[4.1.137.Final] - io.netty:netty-handler:[4.1.137.Final] - io.netty:netty-handler-proxy:[4.1.137.Final] - io.netty:netty-resolver:[4.1.137.Final] - io.netty:netty-resolver-dns:[4.1.137.Final] - io.netty:netty-transport:[4.1.137.Final] + io.netty:netty-buffer:[4.2.17.Final] + io.netty:netty-codec-base:[4.2.17.Final] + io.netty:netty-codec-http:[4.2.17.Final] + io.netty:netty-codec-http2:[4.2.17.Final] + io.netty:netty-common:[4.2.17.Final] + io.netty:netty-handler:[4.2.17.Final] + io.netty:netty-handler-proxy:[4.2.17.Final] + io.netty:netty-resolver:[4.2.17.Final] + io.netty:netty-resolver-dns:[4.2.17.Final] + io.netty:netty-transport:[4.2.17.Final] 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 68c163abd001..dde7adde03c7 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,11 @@ public HttpClient build() { Bootstrap bootstrap = new Bootstrap().group(group) .channel(channelClass) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) getTimeoutMillis(connectTimeout, 10_000)); + // Preserve the pooled default from Netty 4.1 while honoring an explicitly configured Netty allocator. + bootstrap.option(ChannelOption.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 +467,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 f0d7835c190f..4bdffdc966f8 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 2ab0fc5e7223..a9e31d53266b 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 dba6aa10bbea..7a454a7650fe 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 a417ef32f253..3263eeec0f78 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}. @@ -286,12 +300,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 +325,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 +355,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 +364,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 +378,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 +387,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 +473,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 ba84eb3e9035..25f02cd4d45f 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/Netty4UtilityTests.java b/sdk/clientcore/http-netty4/src/test/java/io/clientcore/http/netty4/implementation/Netty4UtilityTests.java index 468f57acffd0..ffa52f66e01c 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/communication/azure-communication-common/pom.xml b/sdk/communication/azure-communication-common/pom.xml index cba34ced14f9..0a06e27dadbc 100644 --- a/sdk/communication/azure-communication-common/pom.xml +++ b/sdk/communication/azure-communication-common/pom.xml @@ -83,7 +83,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/core/azure-core-experimental/pom.xml b/sdk/core/azure-core-experimental/pom.xml index 1ad8ea79775b..0e4b1e740d25 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 f0d76eb2beb4..1c6ea1e4381c 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-netty/CHANGELOG.md b/sdk/core/azure-core-http-netty/CHANGELOG.md index cb59402dcd74..e0a31bd14cf9 100644 --- a/sdk/core/azure-core-http-netty/CHANGELOG.md +++ b/sdk/core/azure-core-http-netty/CHANGELOG.md @@ -8,8 +8,15 @@ ### Bugs Fixed +- Preserved asynchronous file downloads when Netty uses scoped direct buffers on newer JDKs. + ### Other Changes +#### Dependency Updates + +- Migrated Netty dependencies from `4.1.137.Final` to `4.2.17.Final`. +- Upgraded Reactor Netty from `1.2.18` to `1.3.7` and Reactor from `3.7.19` to `3.8.7`. + ## 1.16.7 (2026-08-27) ### Other Changes @@ -809,4 +816,3 @@ This package's [documentation](https://github.com/Azure/azure-sdk-for-java/blob/azure-core-http-netty_1.0.0/sdk/core/azure-core-http-netty/README.md) and [samples](https://github.com/Azure/azure-sdk-for-java/tree/azure-core-http-netty_1.0.0/sdk/core/azure-core-http-netty/src/samples/java/com/azure/core/http/netty) - diff --git a/sdk/core/azure-core-http-netty/README.md b/sdk/core/azure-core-http-netty/README.md index 7369e4c51df3..9808315a9d9f 100644 --- a/sdk/core/azure-core-http-netty/README.md +++ b/sdk/core/azure-core-http-netty/README.md @@ -55,6 +55,17 @@ 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.17.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. + ## Examples The following sections provide several code snippets covering some of the most common client configuration scenarios. @@ -220,5 +231,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/pom.xml b/sdk/core/azure-core-http-netty/pom.xml index d925ee1c8b20..1ccfa18cfd55 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.137.Final + 4.2.17.Final 2.0.81.Final @@ -85,49 +85,49 @@ io.netty netty-handler - 4.1.137.Final + 4.2.17.Final io.netty netty-handler-proxy - 4.1.137.Final + 4.2.17.Final io.netty netty-buffer - 4.1.137.Final + 4.2.17.Final io.netty - netty-codec - 4.1.137.Final + netty-codec-base + 4.2.17.Final io.netty netty-codec-http - 4.1.137.Final + 4.2.17.Final io.netty netty-codec-http2 - 4.1.137.Final + 4.2.17.Final io.netty netty-transport-native-unix-common - 4.1.137.Final + 4.2.17.Final io.netty netty-transport-native-epoll - 4.1.137.Final + 4.2.17.Final linux-x86_64 io.netty netty-transport-native-kqueue - 4.1.137.Final + 4.2.17.Final osx-x86_64 @@ -141,13 +141,13 @@ io.projectreactor.netty reactor-netty-http - 1.2.18 + 1.3.7 io.netty netty-common - 4.1.137.Final + 4.2.17.Final @@ -161,7 +161,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test @@ -209,17 +209,17 @@ io.netty:netty-tcnative-boringssl-static:[2.0.81.Final] - io.projectreactor.netty:reactor-netty-http:[1.2.18] - io.netty:netty-buffer:[4.1.137.Final] - io.netty:netty-common:[4.1.137.Final] - io.netty:netty-codec:[4.1.137.Final] - io.netty:netty-codec-http:[4.1.137.Final] - io.netty:netty-codec-http2:[4.1.137.Final] - io.netty:netty-handler:[4.1.137.Final] - io.netty:netty-handler-proxy:[4.1.137.Final] - io.netty:netty-transport-native-unix-common:[4.1.137.Final] - io.netty:netty-transport-native-epoll:[4.1.137.Final] - io.netty:netty-transport-native-kqueue:[4.1.137.Final] + io.projectreactor.netty:reactor-netty-http:[1.3.7] + io.netty:netty-buffer:[4.2.17.Final] + io.netty:netty-common:[4.2.17.Final] + io.netty:netty-codec-base:[4.2.17.Final] + io.netty:netty-codec-http:[4.2.17.Final] + io.netty:netty-codec-http2:[4.2.17.Final] + io.netty:netty-handler:[4.2.17.Final] + io.netty:netty-handler-proxy:[4.2.17.Final] + io.netty:netty-transport-native-unix-common:[4.2.17.Final] + io.netty:netty-transport-native-epoll:[4.2.17.Final] + io.netty:netty-transport-native-kqueue:[4.2.17.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 1ee286d12e3a..bc69d1485bb2 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,12 @@ 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")) { + 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 +392,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 +411,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 a827169f0e2b..4b4fb0b6c7ef 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 0634e1524126..080aec218ac0 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 a0eba784b12f..d72427d235cb 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,9 @@ 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-http", "netty-codec-http2", "netty-codec-socks"); 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 30d30814abe4..4ac1c23b8ae0 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 58d7ea86ca25..86c33a53652f 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 348e8b1b20f8..0d60c8e6b6f2 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 c6a4f68e1b0e..adb258481a3f 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. */ @@ -524,22 +549,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/implementation/ByteBufWriterSubscriberTests.java b/sdk/core/azure-core-http-netty/src/test/java/com/azure/core/http/netty/implementation/ByteBufWriterSubscriberTests.java index 752bc322d0a4..988ba5467941 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 5a71bba892f8..0b6013caa784 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 d4641d63ada7..14907e8c85b7 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-vertx/README.md b/sdk/core/azure-core-http-vertx/README.md index 2aac5a57e0a0..d9feb3a31861 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 dab6c74dd417..832526f53459 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-management/pom.xml b/sdk/core/azure-core-management/pom.xml index e7c0995ab28e..f94b71feddb5 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 2879c6fb5e02..85534404eabf 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 98886afeb619..55ea4568bc98 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 dcad2265cdac..fb2b70d38c34 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 2bf6fe41ee71..ff09055edbf8 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 51db9ae57da2..25b7f8601991 100644 --- a/sdk/core/azure-core-test/CHANGELOG.md +++ b/sdk/core/azure-core-test/CHANGELOG.md @@ -10,6 +10,8 @@ ### Other Changes +- 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.18 (2026-08-27) ### Other Changes diff --git a/sdk/core/azure-core-test/README.md b/sdk/core/azure-core-test/README.md index c18e6b07642c..cf2de2763ee6 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 b4a9d6c7f58f..6c7b8a832901 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.15] diff --git a/sdk/core/azure-core-tracing-opentelemetry/pom.xml b/sdk/core/azure-core-tracing-opentelemetry/pom.xml index ccfd021becc4..8ce7746ce90f 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 97390b12205a..751d28641bb5 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.9] com.fasterxml.jackson.core:jackson-core:[2.18.9] com.fasterxml.jackson.core:jackson-databind:[2.18.9] diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 4ea9e501ce5a..9076b26d1a60 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.59.1 (2026-08-27) ### Bugs Fixed diff --git a/sdk/core/azure-core/pom.xml b/sdk/core/azure-core/pom.xml index 735261e8a2c5..cbef65d6fd0b 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.9] com.fasterxml.jackson.core:jackson-core:[2.18.9] com.fasterxml.jackson.core:jackson-databind:[2.18.9] diff --git a/sdk/cosmos/azure-cosmos-encryption/pom.xml b/sdk/cosmos/azure-cosmos-encryption/pom.xml index 0c0ec8870d22..f946283f1633 100644 --- a/sdk/cosmos/azure-cosmos-encryption/pom.xml +++ b/sdk/cosmos/azure-cosmos-encryption/pom.xml @@ -178,7 +178,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 489920ab1cee..24f689438cda 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/pom.xml b/sdk/cosmos/azure-cosmos-spark_3/pom.xml index 7613e6539955..b43c2c80a079 100644 --- a/sdk/cosmos/azure-cosmos-spark_3/pom.xml +++ b/sdk/cosmos/azure-cosmos-spark_3/pom.xml @@ -198,7 +198,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/cosmos/azure-cosmos-tests/pom.xml b/sdk/cosmos/azure-cosmos-tests/pom.xml index 24c47db0c392..be44443fb194 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 f8d0a5c0292b..ea7ba842d37f 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 3a5ff7605c8d..84deac4399f4 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 000000000000..ecac817f63f7 --- /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 88b85abbdf68..b867c9fed47c 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 b518db313ee2..6a731f27381b 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 d99a6b198f3d..a259c0f577bd 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -11,6 +11,7 @@ #### Bugs Fixed #### Other Changes +* Updated Netty to `4.2.17.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. ### 4.82.0 (2026-08-26) diff --git a/sdk/cosmos/azure-cosmos/pom.xml b/sdk/cosmos/azure-cosmos/pom.xml index 943713ae8c37..18e08e9306fa 100644 --- a/sdk/cosmos/azure-cosmos/pom.xml +++ b/sdk/cosmos/azure-cosmos/pom.xml @@ -67,12 +67,12 @@ Licensed under the MIT License. com.azure azure-core - 1.59.1 + 1.60.0-beta.1 com.azure azure-core-http-netty - 1.16.7 + 1.17.0-beta.1 com.azure 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 bdb83171ee6e..b152a8f6e3b0 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 78f2e2e19bc1..711cfa9471c6 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 197231a06ea1..72c24016c44e 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 04db3536834c..da8353285529 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 df3dff010fb8..972b20ca5a37 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 f62082edaacc..1753b826c2ab 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,8 @@ 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"; + 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 +139,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 f3faeda09014..1b1ac64ec04f 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/e2e/pom.xml b/sdk/e2e/pom.xml index dbabd6f05bf0..bea1b049a71f 100644 --- a/sdk/e2e/pom.xml +++ b/sdk/e2e/pom.xml @@ -82,7 +82,7 @@ io.projectreactor reactor-test - 3.7.19 + 3.8.7 test diff --git a/sdk/eventhubs/azure-messaging-eventhubs-checkpointstore-jedis/pom.xml b/sdk/eventhubs/azure-messaging-eventhubs-checkpointstore-jedis/pom.xml index 548ae65bada2..165877e265b5 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 ee3fc592a152..a858aa97c129 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/openai/azure-ai-openai-realtime/CHANGELOG.md b/sdk/openai/azure-ai-openai-realtime/CHANGELOG.md index b64e6ac33abd..c1cd59d24165 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.17.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 diff --git a/sdk/openai/azure-ai-openai-realtime/pom.xml b/sdk/openai/azure-ai-openai-realtime/pom.xml index 7eb0ba2df043..ebc65ef3638e 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.59.1 + 1.60.0-beta.1 io.netty netty-codec-http - 4.1.137.Final + 4.2.17.Final @@ -81,7 +81,7 @@ com.azure azure-core-test - 1.27.0-beta.18 + 1.27.0-beta.19 test @@ -102,7 +102,7 @@ - io.netty:netty-codec-http:[4.1.137.Final] + io.netty:netty-codec-http:[4.2.17.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 a033e6c6cac7..8fcd01077fc0 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; @@ -131,7 +132,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 6ea952fb0ced..e53704ff8e5e 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/voicelive/azure-ai-voicelive/pom.xml b/sdk/voicelive/azure-ai-voicelive/pom.xml index f943f33eae2d..50eff6f067b2 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-client/CHANGELOG.md b/sdk/webpubsub/azure-messaging-webpubsub-client/CHANGELOG.md index 1cf59d6b0969..626467ed2946 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.135.Final` to version `4.1.136.Final`. +- Migrated `netty-codec-http` to `4.2.17.Final` and updated WebSocket event loops for Netty 4.2. ## 1.1.10 (2026-08-18) diff --git a/sdk/webpubsub/azure-messaging-webpubsub-client/pom.xml b/sdk/webpubsub/azure-messaging-webpubsub-client/pom.xml index 4127e75641c0..b997dc78ecc6 100644 --- a/sdk/webpubsub/azure-messaging-webpubsub-client/pom.xml +++ b/sdk/webpubsub/azure-messaging-webpubsub-client/pom.xml @@ -48,21 +48,21 @@ com.azure azure-core - 1.59.1 + 1.60.0-beta.1 io.netty netty-codec-http - 4.1.137.Final + 4.2.17.Final com.azure azure-core-test - 1.27.0-beta.18 + 1.27.0-beta.19 test @@ -101,7 +101,7 @@ - io.netty:netty-codec-http:[4.1.137.Final] + io.netty:netty-codec-http:[4.2.17.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 8229e60f16cf..0b449dde5516 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,10 @@ void connect() throws URISyntaxException, SSLException, InterruptedException, Ex Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) + .option(ChannelOption.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 b701da2f86b2..478c4b9eb9a9 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;