Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions reactor-netty-core/build.gradle
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<? super File> 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<? super File> 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() }
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
*
Expand All @@ -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;
Comment on lines +80 to +84
Copy link
Member

Choose a reason for hiding this comment

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

Whether NIO supports the ELG or not, we will return NIO?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do an exhaustive search above so the other option would be to throw an exception that no DefaultLoop was found for the given EventLoopGroup. This would only break if a new EventLoopGroup type is added in the future. What do you suggest?

Copy link
Member

Choose a reason for hiding this comment

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

yes let's throw

}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -235,12 +235,7 @@ default Mono<Void> disposeLater(Duration quietPeriod, Duration timeout) {
* @return a {@link Channel} instance
*/
default <CHANNEL extends Channel> CHANNEL onChannel(Class<CHANNEL> channelType, EventLoopGroup group) {
DefaultLoop channelFactory =
DefaultLoopNativeDetector.INSTANCE.supportGroup(group) ?
DefaultLoopNativeDetector.INSTANCE :
DefaultLoopNativeDetector.NIO;

return channelFactory.getChannel(channelType);
return DefaultLoopNativeDetector.forGroup(group).getChannel(channelType);
}

/**
Expand All @@ -252,12 +247,7 @@ default <CHANNEL extends Channel> CHANNEL onChannel(Class<CHANNEL> channelType,
* @return a {@link Channel} class
*/
default <CHANNEL extends Channel> Class<? extends CHANNEL> onChannelClass(Class<CHANNEL> channelType, EventLoopGroup group) {
DefaultLoop channelFactory =
DefaultLoopNativeDetector.INSTANCE.supportGroup(group) ?
DefaultLoopNativeDetector.INSTANCE :
DefaultLoopNativeDetector.NIO;

return channelFactory.getChannelClass(channelType);
return DefaultLoopNativeDetector.forGroup(group).getChannelClass(channelType);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
Loading
Loading