diff --git a/reactor-netty-core/build.gradle b/reactor-netty-core/build.gradle index 6b866ea5f..8bf3cfdd2 100644 --- a/reactor-netty-core/build.gradle +++ b/reactor-netty-core/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2025 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2026 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -202,12 +202,39 @@ jar { } } +// Use the shadow JAR for java11Test and java17Test to ensure multi-release class loading works correctly. +// The JVM's multi-release feature (selecting classes from META-INF/versions/N/) only works with JAR files, +// not class directories. Without this, both the Java 8 and Java 11+ versions of DefaultLoopIOUring would +// be on the classpath as separate directories, causing unpredictable class loading. By using the shadow JAR, +// the JVM correctly selects the appropriate class version (e.g., Java 11+ io_uring vs Java 8 incubator io_uring). java17Test { - classpath = sourceSets.main.output + sourceSets.test.runtimeClasspath + Set mainOutputs = [ + project.sourceSets.main.output.resourcesDir, + project.sourceSets.main.java.classesDirectory, + ] + classpath = shadowJar.outputs.files + classpath += sourceSets.test.runtimeClasspath.filter { !(it in mainOutputs) } jvmArgs = ["-XX:+AllowRedefinitionToAddDeleteMethods"] + //The imports are not relocated, we do relocation only for the main sources not the tests + exclude '**/*PooledConnectionProviderTest*.*' + //NativeConfigTest uses Reflections to scan for ChannelHandler implementations and compares + //against a static config; shaded classes cause a mismatch + exclude '**/*NativeConfigTest*.*' + dependsOn(shadowJar) } java11Test { - classpath = sourceSets.main.output + sourceSets.test.runtimeClasspath + Set mainOutputs = [ + project.sourceSets.main.output.resourcesDir, + project.sourceSets.main.java.classesDirectory, + ] + classpath = shadowJar.outputs.files + classpath += sourceSets.test.runtimeClasspath.filter { !(it in mainOutputs) } + //The imports are not relocated, we do relocation only for the main sources not the tests + exclude '**/*PooledConnectionProviderTest*.*' + //NativeConfigTest uses Reflections to scan for ChannelHandler implementations and compares + //against a static config; shaded classes cause a mismatch + exclude '**/*NativeConfigTest*.*' + dependsOn(shadowJar) } components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() } @@ -389,6 +416,9 @@ task shadedJarTest(type: Test) { //The imports are not relocated, we do relocation only for the main sources not the tests exclude '**/*PooledConnectionProviderTest*.*' + //NativeConfigTest uses Reflections to scan for ChannelHandler implementations and compares + //against a static config; shaded classes cause a mismatch + exclude '**/*NativeConfigTest*.*' dependsOn(shadowJar) } diff --git a/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopNIO.java b/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopNIO.java index 2059405a1..0499abbe2 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopNIO.java +++ b/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopNIO.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2025 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2026 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import io.netty.channel.Channel; import io.netty.channel.EventLoopGroup; +import io.netty.channel.IoEventLoopGroup; +import io.netty.channel.nio.NioIoHandle; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.SocketChannel; @@ -77,6 +79,9 @@ public EventLoopGroup newEventLoopGroup(int threads, ThreadFactory factory) { @Override public boolean supportGroup(EventLoopGroup group) { - return false; + if (group instanceof ColocatedEventLoopGroup) { + group = ((ColocatedEventLoopGroup) group).get(); + } + return group instanceof IoEventLoopGroup && ((IoEventLoopGroup) group).isCompatible(NioIoHandle.class); } } \ No newline at end of file diff --git a/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopNativeDetector.java b/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopNativeDetector.java index cd7a05d90..4301641e7 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopNativeDetector.java +++ b/reactor-netty-core/src/main/java/reactor/netty/resources/DefaultLoopNativeDetector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2022 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2026 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package reactor.netty.resources; +import io.netty.channel.EventLoopGroup; +import org.jspecify.annotations.Nullable; + /** * Provides an {@link DefaultLoop} instance based on the available transport. * @@ -26,20 +29,58 @@ final class DefaultLoopNativeDetector { static final DefaultLoop NIO; + @Nullable + static final DefaultLoop IO_URING; + + @Nullable + static final DefaultLoop EPOLL; + + @Nullable + static final DefaultLoop KQUEUE; + static { NIO = new DefaultLoopNIO(); + IO_URING = DefaultLoopIOUring.isIoUringAvailable ? new DefaultLoopIOUring() : null; + EPOLL = DefaultLoopEpoll.isEpollAvailable ? new DefaultLoopEpoll() : null; + KQUEUE = DefaultLoopKQueue.isKqueueAvailable ? new DefaultLoopKQueue() : null; - if (DefaultLoopIOUring.isIoUringAvailable) { - INSTANCE = new DefaultLoopIOUring(); + if (IO_URING != null) { + INSTANCE = IO_URING; } - else if (DefaultLoopEpoll.isEpollAvailable) { - INSTANCE = new DefaultLoopEpoll(); + else if (EPOLL != null) { + INSTANCE = EPOLL; } - else if (DefaultLoopKQueue.isKqueueAvailable) { - INSTANCE = new DefaultLoopKQueue(); + else if (KQUEUE != null) { + INSTANCE = KQUEUE; } else { INSTANCE = NIO; } } + + /** + * Finds the correct {@link DefaultLoop} for the given {@link EventLoopGroup}. + * This method checks all available native transports to find one that supports + * the provided group, falling back to NIO if none match. + * + * @param group the event loop group to find a matching channel factory for + * @return the appropriate {@link DefaultLoop} for the group + */ + static DefaultLoop forGroup(EventLoopGroup group) { + // Check each available native transport in priority order + if (IO_URING != null && IO_URING.supportGroup(group)) { + return IO_URING; + } + if (EPOLL != null && EPOLL.supportGroup(group)) { + return EPOLL; + } + if (KQUEUE != null && KQUEUE.supportGroup(group)) { + return KQUEUE; + } + if (NIO.supportGroup(group)) { + return NIO; + } + // Fallback to NIO for unknown group types + return NIO; + } } diff --git a/reactor-netty-core/src/main/java/reactor/netty/resources/LoopResources.java b/reactor-netty-core/src/main/java/reactor/netty/resources/LoopResources.java index a5a92263b..819b7a5c7 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/resources/LoopResources.java +++ b/reactor-netty-core/src/main/java/reactor/netty/resources/LoopResources.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2025 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-2026 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -235,12 +235,7 @@ default Mono disposeLater(Duration quietPeriod, Duration timeout) { * @return a {@link Channel} instance */ default CHANNEL onChannel(Class channelType, EventLoopGroup group) { - DefaultLoop channelFactory = - DefaultLoopNativeDetector.INSTANCE.supportGroup(group) ? - DefaultLoopNativeDetector.INSTANCE : - DefaultLoopNativeDetector.NIO; - - return channelFactory.getChannel(channelType); + return DefaultLoopNativeDetector.forGroup(group).getChannel(channelType); } /** @@ -252,12 +247,7 @@ default CHANNEL onChannel(Class channelType, * @return a {@link Channel} class */ default Class onChannelClass(Class channelType, EventLoopGroup group) { - DefaultLoop channelFactory = - DefaultLoopNativeDetector.INSTANCE.supportGroup(group) ? - DefaultLoopNativeDetector.INSTANCE : - DefaultLoopNativeDetector.NIO; - - return channelFactory.getChannelClass(channelType); + return DefaultLoopNativeDetector.forGroup(group).getChannelClass(channelType); } /** diff --git a/reactor-netty-core/src/main/java17/reactor/netty/resources/DefaultLoopNIO.java b/reactor-netty-core/src/main/java17/reactor/netty/resources/DefaultLoopNIO.java index 15e050929..25ced4eee 100644 --- a/reactor-netty-core/src/main/java17/reactor/netty/resources/DefaultLoopNIO.java +++ b/reactor-netty-core/src/main/java17/reactor/netty/resources/DefaultLoopNIO.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2025 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2026 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import io.netty.channel.Channel; import io.netty.channel.EventLoopGroup; +import io.netty.channel.IoEventLoopGroup; +import io.netty.channel.nio.NioIoHandle; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.SocketChannel; @@ -87,6 +89,9 @@ public EventLoopGroup newEventLoopGroup(int threads, ThreadFactory factory) { @Override public boolean supportGroup(EventLoopGroup group) { - return false; + if (group instanceof ColocatedEventLoopGroup) { + group = ((ColocatedEventLoopGroup) group).get(); + } + return group instanceof IoEventLoopGroup && ((IoEventLoopGroup) group).isCompatible(NioIoHandle.class); } } \ No newline at end of file diff --git a/reactor-netty-core/src/test/java/reactor/netty/resources/DefaultLoopResourcesTest.java b/reactor-netty-core/src/test/java/reactor/netty/resources/DefaultLoopResourcesTest.java index 40cd13e2f..78fef90f3 100644 --- a/reactor-netty-core/src/test/java/reactor/netty/resources/DefaultLoopResourcesTest.java +++ b/reactor-netty-core/src/test/java/reactor/netty/resources/DefaultLoopResourcesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2025 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2026 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,24 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.MultiThreadIoEventLoopGroup; import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollDatagramChannel; +import io.netty.channel.epoll.EpollIoHandler; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.epoll.EpollSocketChannel; import io.netty.channel.kqueue.KQueue; +import io.netty.channel.kqueue.KQueueDatagramChannel; +import io.netty.channel.kqueue.KQueueIoHandler; +import io.netty.channel.kqueue.KQueueServerSocketChannel; +import io.netty.channel.kqueue.KQueueSocketChannel; import io.netty.channel.nio.NioIoHandler; -import io.netty.channel.uring.IoUring; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.channel.uring.IoUringIoHandler; +import io.netty.incubator.channel.uring.IOUringEventLoopGroup; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.EnabledOnJre; @@ -216,7 +231,7 @@ void testEpollIsAvailable() { void testIoUringIsAvailable() { boolean isTransportIoUring = "io_uring".equals(System.getProperty("forceTransport")); assumeThat(isTransportIoUring).isTrue(); - assertThat(IoUring.isAvailable()).isTrue(); + assertThat(io.netty.channel.uring.IoUring.isAvailable()).isTrue(); } @Test @@ -234,4 +249,292 @@ void testKQueueIsAvailable() { assumeThat(System.getProperty("forceTransport")).isEqualTo("native"); assertThat(KQueue.isAvailable()).isTrue(); } + + // ============== Epoll Transport Tests (Linux) ============== + + @Test + @EnabledOnOs(OS.LINUX) + void testOnChannelWithEpollEventLoopGroup() throws Exception { + assumeThat(Epoll.isAvailable()).isTrue(); + + EventLoopGroup epollGroup = new MultiThreadIoEventLoopGroup(1, EpollIoHandler.newFactory()); + try { + LoopResources loopResources = LoopResources.create("testOnChannelEpoll"); + try { + // Verify onChannel returns correct Epoll channel instances + SocketChannel socketChannel = loopResources.onChannel(SocketChannel.class, epollGroup); + assertThat(socketChannel).isInstanceOf(EpollSocketChannel.class); + + ServerSocketChannel serverSocketChannel = loopResources.onChannel(ServerSocketChannel.class, epollGroup); + assertThat(serverSocketChannel).isInstanceOf(EpollServerSocketChannel.class); + + DatagramChannel datagramChannel = loopResources.onChannel(DatagramChannel.class, epollGroup); + assertThat(datagramChannel).isInstanceOf(EpollDatagramChannel.class); + } + finally { + loopResources.disposeLater().block(Duration.ofSeconds(5)); + } + } + finally { + epollGroup.shutdownGracefully().get(5, TimeUnit.SECONDS); + } + } + + @Test + @EnabledOnOs(OS.LINUX) + void testOnChannelClassWithEpollEventLoopGroup() throws Exception { + assumeThat(Epoll.isAvailable()).isTrue(); + + EventLoopGroup epollGroup = new MultiThreadIoEventLoopGroup(1, EpollIoHandler.newFactory()); + try { + LoopResources loopResources = LoopResources.create("testOnChannelClassEpoll"); + try { + // Verify onChannelClass returns correct Epoll channel classes + Class socketChannelClass = loopResources.onChannelClass(SocketChannel.class, epollGroup); + assertThat(socketChannelClass).isEqualTo(EpollSocketChannel.class); + + Class serverSocketChannelClass = loopResources.onChannelClass(ServerSocketChannel.class, epollGroup); + assertThat(serverSocketChannelClass).isEqualTo(EpollServerSocketChannel.class); + + Class datagramChannelClass = loopResources.onChannelClass(DatagramChannel.class, epollGroup); + assertThat(datagramChannelClass).isEqualTo(EpollDatagramChannel.class); + } + finally { + loopResources.disposeLater().block(Duration.ofSeconds(5)); + } + } + finally { + epollGroup.shutdownGracefully().get(5, TimeUnit.SECONDS); + } + } + + // ============== KQueue Transport Tests (macOS) ============== + + @Test + @EnabledOnOs(OS.MAC) + void testOnChannelWithKQueueEventLoopGroup() throws Exception { + assumeThat(KQueue.isAvailable()).isTrue(); + + EventLoopGroup kqueueGroup = new MultiThreadIoEventLoopGroup(1, KQueueIoHandler.newFactory()); + try { + LoopResources loopResources = LoopResources.create("testOnChannelKQueue"); + try { + // Verify onChannel returns correct KQueue channel instances + SocketChannel socketChannel = loopResources.onChannel(SocketChannel.class, kqueueGroup); + assertThat(socketChannel).isInstanceOf(KQueueSocketChannel.class); + + ServerSocketChannel serverSocketChannel = loopResources.onChannel(ServerSocketChannel.class, kqueueGroup); + assertThat(serverSocketChannel).isInstanceOf(KQueueServerSocketChannel.class); + + DatagramChannel datagramChannel = loopResources.onChannel(DatagramChannel.class, kqueueGroup); + assertThat(datagramChannel).isInstanceOf(KQueueDatagramChannel.class); + } + finally { + loopResources.disposeLater().block(Duration.ofSeconds(5)); + } + } + finally { + kqueueGroup.shutdownGracefully().get(5, TimeUnit.SECONDS); + } + } + + @Test + @EnabledOnOs(OS.MAC) + void testOnChannelClassWithKQueueEventLoopGroup() throws Exception { + assumeThat(KQueue.isAvailable()).isTrue(); + + EventLoopGroup kqueueGroup = new MultiThreadIoEventLoopGroup(1, KQueueIoHandler.newFactory()); + try { + LoopResources loopResources = LoopResources.create("testOnChannelClassKQueue"); + try { + // Verify onChannelClass returns correct KQueue channel classes + Class socketChannelClass = loopResources.onChannelClass(SocketChannel.class, kqueueGroup); + assertThat(socketChannelClass).isEqualTo(KQueueSocketChannel.class); + + Class serverSocketChannelClass = loopResources.onChannelClass(ServerSocketChannel.class, kqueueGroup); + assertThat(serverSocketChannelClass).isEqualTo(KQueueServerSocketChannel.class); + + Class datagramChannelClass = loopResources.onChannelClass(DatagramChannel.class, kqueueGroup); + assertThat(datagramChannelClass).isEqualTo(KQueueDatagramChannel.class); + } + finally { + loopResources.disposeLater().block(Duration.ofSeconds(5)); + } + } + finally { + kqueueGroup.shutdownGracefully().get(5, TimeUnit.SECONDS); + } + } + + // ============== io_uring Transport Tests (Linux, Java 11+) ============== + + @Test + @EnabledOnOs(OS.LINUX) + @EnabledForJreRange(min = JRE.JAVA_11) + void testOnChannelWithIoUringEventLoopGroup() throws Exception { + assumeThat(io.netty.channel.uring.IoUring.isAvailable()).isTrue(); + + EventLoopGroup ioUringGroup = new MultiThreadIoEventLoopGroup(1, IoUringIoHandler.newFactory()); + try { + LoopResources loopResources = LoopResources.create("testOnChannelIoUring"); + try { + // Verify onChannel returns correct io_uring channel instances (Java 11+) + SocketChannel socketChannel = loopResources.onChannel(SocketChannel.class, ioUringGroup); + assertThat(socketChannel).isInstanceOf(io.netty.channel.uring.IoUringSocketChannel.class); + + ServerSocketChannel serverSocketChannel = loopResources.onChannel(ServerSocketChannel.class, ioUringGroup); + assertThat(serverSocketChannel).isInstanceOf(io.netty.channel.uring.IoUringServerSocketChannel.class); + + DatagramChannel datagramChannel = loopResources.onChannel(DatagramChannel.class, ioUringGroup); + assertThat(datagramChannel).isInstanceOf(io.netty.channel.uring.IoUringDatagramChannel.class); + } + finally { + loopResources.disposeLater().block(Duration.ofSeconds(5)); + } + } + finally { + ioUringGroup.shutdownGracefully().get(5, TimeUnit.SECONDS); + } + } + + @Test + @EnabledOnOs(OS.LINUX) + @EnabledForJreRange(min = JRE.JAVA_11) + void testOnChannelClassWithIoUringEventLoopGroup() throws Exception { + assumeThat(io.netty.channel.uring.IoUring.isAvailable()).isTrue(); + + EventLoopGroup ioUringGroup = new MultiThreadIoEventLoopGroup(1, IoUringIoHandler.newFactory()); + try { + LoopResources loopResources = LoopResources.create("testOnChannelClassIoUring"); + try { + // Verify onChannelClass returns correct io_uring channel classes (Java 11+) + Class socketChannelClass = loopResources.onChannelClass(SocketChannel.class, ioUringGroup); + assertThat(socketChannelClass).isEqualTo(io.netty.channel.uring.IoUringSocketChannel.class); + + Class serverSocketChannelClass = loopResources.onChannelClass(ServerSocketChannel.class, ioUringGroup); + assertThat(serverSocketChannelClass).isEqualTo(io.netty.channel.uring.IoUringServerSocketChannel.class); + + Class datagramChannelClass = loopResources.onChannelClass(DatagramChannel.class, ioUringGroup); + assertThat(datagramChannelClass).isEqualTo(io.netty.channel.uring.IoUringDatagramChannel.class); + } + finally { + loopResources.disposeLater().block(Duration.ofSeconds(5)); + } + } + finally { + ioUringGroup.shutdownGracefully().get(5, TimeUnit.SECONDS); + } + } + + // ============== io_uring Transport Tests (Linux, Java 8 incubator) ============== + + @Test + @EnabledOnOs(OS.LINUX) + @EnabledOnJre(JRE.JAVA_8) + void testOnChannelWithIoUringIncubatorEventLoopGroup() throws Exception { + assumeThat(io.netty.incubator.channel.uring.IOUring.isAvailable()).isTrue(); + + EventLoopGroup ioUringGroup = new IOUringEventLoopGroup(1); + try { + LoopResources loopResources = LoopResources.create("testOnChannelIoUringIncubator"); + try { + // Verify onChannel returns correct io_uring incubator channel instances (Java 8) + SocketChannel socketChannel = loopResources.onChannel(SocketChannel.class, ioUringGroup); + assertThat(socketChannel).isInstanceOf(io.netty.incubator.channel.uring.IOUringSocketChannel.class); + + ServerSocketChannel serverSocketChannel = loopResources.onChannel(ServerSocketChannel.class, ioUringGroup); + assertThat(serverSocketChannel).isInstanceOf(io.netty.incubator.channel.uring.IOUringServerSocketChannel.class); + + DatagramChannel datagramChannel = loopResources.onChannel(DatagramChannel.class, ioUringGroup); + assertThat(datagramChannel).isInstanceOf(io.netty.incubator.channel.uring.IOUringDatagramChannel.class); + } + finally { + loopResources.disposeLater().block(Duration.ofSeconds(5)); + } + } + finally { + ioUringGroup.shutdownGracefully().get(5, TimeUnit.SECONDS); + } + } + + @Test + @EnabledOnOs(OS.LINUX) + @EnabledOnJre(JRE.JAVA_8) + void testOnChannelClassWithIoUringIncubatorEventLoopGroup() throws Exception { + assumeThat(io.netty.incubator.channel.uring.IOUring.isAvailable()).isTrue(); + + EventLoopGroup ioUringGroup = new IOUringEventLoopGroup(1); + try { + LoopResources loopResources = LoopResources.create("testOnChannelClassIoUringIncubator"); + try { + // Verify onChannelClass returns correct io_uring incubator channel classes (Java 8) + Class socketChannelClass = loopResources.onChannelClass(SocketChannel.class, ioUringGroup); + assertThat(socketChannelClass).isEqualTo(io.netty.incubator.channel.uring.IOUringSocketChannel.class); + + Class serverSocketChannelClass = loopResources.onChannelClass(ServerSocketChannel.class, ioUringGroup); + assertThat(serverSocketChannelClass).isEqualTo(io.netty.incubator.channel.uring.IOUringServerSocketChannel.class); + + Class datagramChannelClass = loopResources.onChannelClass(DatagramChannel.class, ioUringGroup); + assertThat(datagramChannelClass).isEqualTo(io.netty.incubator.channel.uring.IOUringDatagramChannel.class); + } + finally { + loopResources.disposeLater().block(Duration.ofSeconds(5)); + } + } + finally { + ioUringGroup.shutdownGracefully().get(5, TimeUnit.SECONDS); + } + } + + // ============== NIO Transport Tests (All Platforms) ============== + + @Test + void testOnChannelWithNioEventLoopGroup() throws Exception { + EventLoopGroup nioGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); + try { + LoopResources loopResources = LoopResources.create("testOnChannelNio"); + try { + // Verify onChannel returns correct NIO channel instances + SocketChannel socketChannel = loopResources.onChannel(SocketChannel.class, nioGroup); + assertThat(socketChannel).isInstanceOf(NioSocketChannel.class); + + ServerSocketChannel serverSocketChannel = loopResources.onChannel(ServerSocketChannel.class, nioGroup); + assertThat(serverSocketChannel).isInstanceOf(NioServerSocketChannel.class); + + DatagramChannel datagramChannel = loopResources.onChannel(DatagramChannel.class, nioGroup); + assertThat(datagramChannel).isInstanceOf(NioDatagramChannel.class); + } + finally { + loopResources.disposeLater().block(Duration.ofSeconds(5)); + } + } + finally { + nioGroup.shutdownGracefully().get(5, TimeUnit.SECONDS); + } + } + + @Test + void testOnChannelClassWithNioEventLoopGroup() throws Exception { + EventLoopGroup nioGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); + try { + LoopResources loopResources = LoopResources.create("testOnChannelClassNio"); + try { + // Verify onChannelClass returns correct NIO channel classes + Class socketChannelClass = loopResources.onChannelClass(SocketChannel.class, nioGroup); + assertThat(socketChannelClass).isEqualTo(NioSocketChannel.class); + + Class serverSocketChannelClass = loopResources.onChannelClass(ServerSocketChannel.class, nioGroup); + assertThat(serverSocketChannelClass).isEqualTo(NioServerSocketChannel.class); + + Class datagramChannelClass = loopResources.onChannelClass(DatagramChannel.class, nioGroup); + assertThat(datagramChannelClass).isEqualTo(NioDatagramChannel.class); + } + finally { + loopResources.disposeLater().block(Duration.ofSeconds(5)); + } + } + finally { + nioGroup.shutdownGracefully().get(5, TimeUnit.SECONDS); + } + } } \ No newline at end of file