diff --git a/bootstrap/bungeecord/build.gradle.kts b/bootstrap/bungeecord/build.gradle.kts index 1564b7f75..b38bfcd6f 100644 --- a/bootstrap/bungeecord/build.gradle.kts +++ b/bootstrap/bungeecord/build.gradle.kts @@ -6,6 +6,8 @@ plugins { dependencies { api(projects.core) + compileOnly(libs.netty.transport.native.io.uring) + implementation(libs.cloud.bungee) implementation(libs.adventure.text.serializer.bungeecord) compileOnlyApi(libs.bungeecord.proxy) diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java index 459bde3af..452ab968f 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java @@ -26,12 +26,28 @@ package org.geysermc.geyser.platform.bungeecord; import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; +import io.netty.channel.IoEventLoop; +import io.netty.channel.IoEventLoopGroup; +import io.netty.channel.IoHandler; +import io.netty.channel.IoHandlerFactory; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.SingleThreadIoEventLoop; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollIoHandler; import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalIoHandler; +import io.netty.channel.nio.NioIoHandler; +import io.netty.channel.uring.IoUring; +import io.netty.channel.uring.IoUringIoHandler; import io.netty.util.AttributeKey; +import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.concurrent.ThreadAwareExecutor; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.event.ProxyReloadEvent; @@ -43,12 +59,15 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.network.netty.GeyserInjector; +import org.geysermc.geyser.network.netty.IoHandlerWrapper; import org.geysermc.geyser.network.netty.LocalServerChannelWrapper; import org.geysermc.geyser.network.netty.LocalSession; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; public class GeyserBungeeInjector extends GeyserInjector implements Listener { private final Plugin plugin; @@ -74,6 +93,13 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener { "Please reach out to us on our Discord at https://discord.gg/GeyserMC so we can hear feedback on your setup."); } + try { + Class.forName("io.netty.channel.MultiThreadIoEventLoopGroup"); + } catch (ClassNotFoundException e) { + bootstrap.getGeyserLogger().error("use-direct-connection disabled as BungeeCord is not up-to-date."); + return; + } + // TODO remove try { ProxyServer.class.getMethod("unsafe"); @@ -85,22 +111,39 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener { Class proxyClass = proxy.getClass(); // Using the specified EventLoop is required, or else an error will be thrown - EventLoopGroup bossGroup; - EventLoopGroup workerGroup; + MultiThreadIoEventLoopGroup workerGroup; try { - EventLoopGroup eventLoops = (EventLoopGroup) proxyClass.getField("eventLoops").get(proxy); - // Netty redirects ServerBootstrap#group(EventLoopGroup) to #group(EventLoopGroup, EventLoopGroup) and uses the same event loop for both. - bossGroup = eventLoops; - workerGroup = eventLoops; + workerGroup = (MultiThreadIoEventLoopGroup) proxyClass.getField("eventLoops").get(proxy); bootstrap.getGeyserLogger().debug("BungeeCord event loop style detected."); } catch (NoSuchFieldException e) { // Waterfall uses two separate event loops // https://github.com/PaperMC/Waterfall/blob/fea7ec356dba6c6ac28819ff11be604af6eb484e/BungeeCord-Patches/0022-Use-a-worker-and-a-boss-event-loop-group.patch - bossGroup = (EventLoopGroup) proxyClass.getField("bossEventLoopGroup").get(proxy); - workerGroup = (EventLoopGroup) proxyClass.getField("workerEventLoopGroup").get(proxy); + workerGroup = (MultiThreadIoEventLoopGroup) proxyClass.getField("workerEventLoopGroup").get(proxy); bootstrap.getGeyserLogger().debug("Waterfall event loop style detected."); } + final IoEventLoopGroup finalWorkerGroup = workerGroup; + var factory = LocalIoHandler.newFactory(); + var nativeFactory = getNativeHandlerFactory(); + var wrapperFactory = new IoHandlerFactory() { + @Override + public IoHandler newHandler(ThreadAwareExecutor ioExecutor) { + return new IoHandlerWrapper(factory.newHandler(ioExecutor), nativeFactory.newHandler(ioExecutor)); + } + }; + + EventLoopGroup wrapperGroup = new MultiThreadIoEventLoopGroup(factory) { + @Override + protected ThreadFactory newDefaultThreadFactory() { + return new DefaultThreadFactory("Geyser Backend Worker Group", Thread.MAX_PRIORITY); + } + + @Override + protected IoEventLoop newChild(Executor executor, IoHandlerFactory ioHandlerFactory, Object... args) { + return new SingleThreadIoEventLoop(finalWorkerGroup, executor, wrapperFactory); + } + }; + // Is currently just AttributeKey.valueOf("ListerInfo") but we might as well copy the value itself. AttributeKey listener = PipelineUtils.LISTENER; listenerInfo = new ListenerInfo( @@ -157,7 +200,7 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener { } }) .childAttr(listener, listenerInfo) - .group(bossGroup, workerGroup) + .group(new MultiThreadIoEventLoopGroup(LocalIoHandler.newFactory()), wrapperGroup) .localAddress(LocalAddress.ANY)) .bind() .syncUninterruptibly(); @@ -202,4 +245,16 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener { initializeLocalChannel(GeyserImpl.getInstance().getBootstrap()); } } + + private static IoHandlerFactory getNativeHandlerFactory() { + // Match whatever settings the Bungee proxy is using + // https://github.com/SpigotMC/BungeeCord/blob/617c2728a25347487eee4e8649d52fe57f1ff6e2/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java#L139-L162 + if (Boolean.parseBoolean(System.getProperty("bungee.io_uring", "false")) && IoUring.isAvailable()) { + return IoUringIoHandler.newFactory(); + } + if (Boolean.parseBoolean(System.getProperty("bungee.epoll", "true")) && Epoll.isAvailable()) { + return EpollIoHandler.newFactory(); + } + return NioIoHandler.newFactory(); + } } diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java index 918c13b93..af14e801f 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java @@ -25,6 +25,8 @@ package org.geysermc.geyser.platform.bungeecord; +import io.netty.buffer.AdaptiveByteBufAllocator; +import io.netty.buffer.ByteBufAllocator; import io.netty.channel.Channel; import net.md_5.bungee.BungeeCord; import net.md_5.bungee.api.CommandSender; @@ -82,9 +84,12 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap { public void onGeyserInitialize() { GeyserLocale.init(this); + // TODO remove when this isn't an issue anymore + boolean adaptiveAllocatorUsed = System.getProperty("io.netty.allocator.type") == null && ByteBufAllocator.DEFAULT instanceof AdaptiveByteBufAllocator; + try { List supportedProtocols = ProtocolConstants.SUPPORTED_VERSION_IDS; - if (!supportedProtocols.contains(GameProtocol.getJavaProtocolVersion())) { + if (!supportedProtocols.contains(GameProtocol.getJavaProtocolVersion()) || adaptiveAllocatorUsed) { geyserLogger.error(" / \\"); geyserLogger.error(" / \\"); geyserLogger.error(" / | \\"); diff --git a/bootstrap/standalone/build.gradle.kts b/bootstrap/standalone/build.gradle.kts index 5a13056eb..d463fa7e1 100644 --- a/bootstrap/standalone/build.gradle.kts +++ b/bootstrap/standalone/build.gradle.kts @@ -32,6 +32,11 @@ tasks.named("jar") { tasks.withType { archiveBaseName.set("Geyser-Standalone") + // temporary measure - incubator's io_uring is not compatible with 4.2.1 + dependencies { + exclude(dependency("io.netty.incubator:.*")) + } + transform(Log4j2PluginsCacheFileTransformer()) } diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java index 5bdb206db..03ae18408 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java @@ -89,6 +89,11 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED); // Can eat performance } + // Restore allocator used before Netty 4.2 due to oom issues with the adaptive allocator + if (System.getProperty("io.netty.allocator.type") == null) { + System.setProperty("io.netty.allocator.type", "pooled"); + } + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); GeyserStandaloneLogger.setupStreams(); diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java index e0550b416..c8a850131 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java @@ -27,15 +27,29 @@ package org.geysermc.geyser.platform.velocity; import com.velocitypowered.api.proxy.ProxyServer; import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.IoEventLoop; +import io.netty.channel.IoHandlerFactory; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.local.LocalAddress; +import io.netty.channel.local.LocalIoHandler; +import io.netty.util.concurrent.DefaultThreadFactory; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.GeyserBootstrap; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.network.netty.GeyserInjector; import org.geysermc.geyser.network.netty.LocalServerChannelWrapper; +import org.geysermc.geyser.network.netty.WatchedSingleThreadIoEventLoop; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; import java.util.function.Supplier; public class GeyserVelocityInjector extends GeyserInjector { @@ -48,11 +62,11 @@ public class GeyserVelocityInjector extends GeyserInjector { @Override @SuppressWarnings("unchecked") protected void initializeLocalChannel0(GeyserBootstrap bootstrap) throws Exception { - // TEMPORARY until Netty 4.2 is implemented with these changes in mind. try { Class.forName("io.netty.channel.MultiThreadIoEventLoopGroup"); + } catch (ClassNotFoundException e) { + bootstrap.getGeyserLogger().error("use-direct-connection disabled as Velocity is not up-to-date."); return; - } catch (ClassNotFoundException ignored) { } Field cm = proxy.getClass().getDeclaredField("cm"); @@ -70,34 +84,44 @@ public class GeyserVelocityInjector extends GeyserInjector { serverWriteMarkField.setAccessible(true); WriteBufferWaterMark serverWriteMark = (WriteBufferWaterMark) serverWriteMarkField.get(null); - EventLoopGroup bossGroup = (EventLoopGroup) connectionManagerClass.getMethod("getBossGroup").invoke(connectionManager); - Field workerGroupField = connectionManagerClass.getDeclaredField("workerGroup"); workerGroupField.setAccessible(true); EventLoopGroup workerGroup = (EventLoopGroup) workerGroupField.get(connectionManager); + EventLoopGroup wrapperGroup = new MultiThreadIoEventLoopGroup(LocalIoHandler.newFactory()) { + @Override + protected ThreadFactory newDefaultThreadFactory() { + return new DefaultThreadFactory("Geyser Backend Worker Group", Thread.MAX_PRIORITY); + } + + @Override + protected IoEventLoop newChild(Executor executor, IoHandlerFactory ioHandlerFactory, Object... args) { + return new WatchedSingleThreadIoEventLoop(workerGroup, this, executor, ioHandlerFactory); + } + }; + // This method is what initializes the connection in Java Edition, after Netty is all set. Method initChannel = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class); initChannel.setAccessible(true); ChannelFuture channelFuture = (new ServerBootstrap() - .channel(LocalServerChannelWrapper.class) - .childHandler(new ChannelInitializer<>() { - @Override - protected void initChannel(@NonNull Channel ch) throws Exception { - initChannel.invoke(channelInitializer, ch); + .channel(LocalServerChannelWrapper.class) + .childHandler(new ChannelInitializer<>() { + @Override + protected void initChannel(@NonNull Channel ch) throws Exception { + initChannel.invoke(channelInitializer, ch); - if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserVelocityCompressionDisabler.ENABLED) { - ch.pipeline().addAfter("minecraft-encoder", "geyser-compression-disabler", - new GeyserVelocityCompressionDisabler()); - } + if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserVelocityCompressionDisabler.ENABLED) { + ch.pipeline().addAfter("minecraft-encoder", "geyser-compression-disabler", + new GeyserVelocityCompressionDisabler()); } - }) - .group(bossGroup, workerGroup) // Cannot be DefaultEventLoopGroup - .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverWriteMark) // Required or else rare network freezes can occur - .localAddress(LocalAddress.ANY)) - .bind() - .syncUninterruptibly(); + } + }) + .group(new MultiThreadIoEventLoopGroup(LocalIoHandler.newFactory()), wrapperGroup) // Cannot be DefaultEventLoopGroup + .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverWriteMark) // Required or else rare network freezes can occur + .localAddress(LocalAddress.ANY)) + .bind() + .syncUninterruptibly(); this.localChannel = channelFuture; this.serverSocketAddress = channelFuture.channel().localAddress(); diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java index 8fa47f569..0e6150b87 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java @@ -36,6 +36,8 @@ import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.proxy.ProxyServer; +import io.netty.buffer.AdaptiveByteBufAllocator; +import io.netty.buffer.ByteBufAllocator; import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -91,7 +93,10 @@ public class GeyserVelocityPlugin implements GeyserBootstrap { public void onGeyserInitialize() { GeyserLocale.init(this); - if (!ProtocolVersion.isSupported(GameProtocol.getJavaProtocolVersion())) { + // TODO remove when this isn't an issue anymore + boolean adaptiveAllocatorUsed = System.getProperty("io.netty.allocator.type") == null && ByteBufAllocator.DEFAULT instanceof AdaptiveByteBufAllocator; + + if (!ProtocolVersion.isSupported(GameProtocol.getJavaProtocolVersion()) || adaptiveAllocatorUsed) { geyserLogger.error(" / \\"); geyserLogger.error(" / \\"); geyserLogger.error(" / | \\"); diff --git a/core/build.gradle.kts b/core/build.gradle.kts index b0ea5fdf6..d0443dc9c 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -43,11 +43,11 @@ dependencies { // Network dependencies we are updating ourselves api(libs.netty.handler) - implementation(libs.netty.transport.native.epoll) { artifact { classifier = "linux-x86_64" } } + api(libs.netty.transport.native.epoll) { artifact { classifier = "linux-x86_64" } } implementation(libs.netty.transport.native.epoll) { artifact { classifier = "linux-aarch_64" } } implementation(libs.netty.transport.native.kqueue) { artifact { classifier = "osx-x86_64" } } - implementation(libs.netty.transport.native.io.uring) { artifact { classifier = "linux-x86_64" } } - implementation(libs.netty.transport.native.io.uring) { artifact { classifier = "linux-aarch_64" } } + //api(libs.netty.transport.native.io.uring) { artifact { classifier = "linux-x86_64" } } + //implementation(libs.netty.transport.native.io.uring) { artifact { classifier = "linux-aarch_64" } } // Adventure text serialization api(libs.bundles.adventure) diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java index fec254996..9005783ab 100644 --- a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java +++ b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java @@ -38,9 +38,6 @@ import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.nio.NioDatagramChannel; -import io.netty.incubator.channel.uring.IOUring; -import io.netty.incubator.channel.uring.IOUringDatagramChannel; -import io.netty.incubator.channel.uring.IOUringEventLoopGroup; import io.netty.util.concurrent.Future; import lombok.Getter; import net.jodah.expiringmap.ExpirationPolicy; @@ -434,11 +431,12 @@ public final class GeyserServer { } private static Transport compatibleTransport() { - if (isClassAvailable("io.netty.incubator.channel.uring.IOUring") - && IOUring.isAvailable() - && Boolean.parseBoolean(System.getProperty("Geyser.io_uring"))) { - return new Transport(IOUringDatagramChannel.class, IOUringEventLoopGroup::new); - } + // FIXME supporting io_uring post 4.2 requires more changes +// if (isClassAvailable("io.netty.incubator.channel.uring.IOUring") +// && IOUring.isAvailable() +// && Boolean.parseBoolean(System.getProperty("Geyser.io_uring"))) { +// return new Transport(IOUringDatagramChannel.class, IOUringEventLoopGroup::new); +// } if (isClassAvailable("io.netty.channel.epoll.Epoll") && Epoll.isAvailable()) { return new Transport(EpollDatagramChannel.class, EpollEventLoopGroup::new); diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/IoHandlerWrapper.java b/core/src/main/java/org/geysermc/geyser/network/netty/IoHandlerWrapper.java new file mode 100644 index 000000000..70a34aa0d --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/netty/IoHandlerWrapper.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network.netty; + +import io.netty.channel.IoHandle; +import io.netty.channel.IoHandler; +import io.netty.channel.IoHandlerContext; +import io.netty.channel.IoRegistration; + +public class IoHandlerWrapper implements IoHandler { + private final IoHandler localHandler; + private final IoHandler nativeHandler; + private final IoHandlerContextWrapper contextWrapper = new IoHandlerContextWrapper(); + + public IoHandlerWrapper(IoHandler localHandler, IoHandler nativeHandler) { + this.localHandler = localHandler; + this.nativeHandler = nativeHandler; + } + + @Override + public void initialize() { + localHandler.initialize(); + nativeHandler.initialize(); + } + + @Override + public int run(IoHandlerContext context) { + contextWrapper.base = context; + localHandler.run(contextWrapper); + return nativeHandler.run(context); + } + + private static class IoHandlerContextWrapper implements IoHandlerContext { + private IoHandlerContext base; + + @Override + public boolean canBlock() { + // https://github.com/netty/netty/blob/7ac5e2ba20030caad0d0e648e86f31e9f0461105/transport/src/main/java/io/netty/channel/local/LocalIoHandler.java#L63 + // will block the other IoHandler. + return false; + } + + @Override + public long delayNanos(long currentTimeNanos) { + return base.delayNanos(currentTimeNanos); + } + + @Override + public long deadlineNanos() { + return base.deadlineNanos(); + } + } + + @Override + public void prepareToDestroy() { + localHandler.prepareToDestroy(); + nativeHandler.prepareToDestroy(); + } + + @Override + public void destroy() { + localHandler.destroy(); + nativeHandler.destroy(); + } + + private static final Class LOCAL_HANDLER_CLASS; + + static { + try { + LOCAL_HANDLER_CLASS = (Class) Class.forName("io.netty.channel.local.LocalIoHandle"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + @Override + public IoRegistration register(IoHandle handle) throws Exception { + if (LOCAL_HANDLER_CLASS.isAssignableFrom(handle.getClass())) { + return this.localHandler.register(handle); + } + return this.nativeHandler.register(handle); + } + + @Override + public void wakeup() { + localHandler.wakeup(); + nativeHandler.wakeup(); + } + + @Override + public boolean isCompatible(Class handleType) { + return localHandler.isCompatible(handleType) || nativeHandler.isCompatible(handleType); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/WatchedSingleThreadIoEventLoop.java b/core/src/main/java/org/geysermc/geyser/network/netty/WatchedSingleThreadIoEventLoop.java new file mode 100644 index 000000000..7de8cd18c --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/network/netty/WatchedSingleThreadIoEventLoop.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.network.netty; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.IoEventLoopGroup; +import io.netty.channel.IoHandlerFactory; +import io.netty.channel.SingleThreadIoEventLoop; + +import java.util.concurrent.Executor; + +public class WatchedSingleThreadIoEventLoop extends SingleThreadIoEventLoop { + private final EventLoopGroup trueWorkerGroup; + + public WatchedSingleThreadIoEventLoop(EventLoopGroup trueWorkerGroup, IoEventLoopGroup parent, + Executor executor, IoHandlerFactory ioHandlerFactory) { + super(parent, executor, ioHandlerFactory); + this.trueWorkerGroup = trueWorkerGroup; + } + + @Override + @SuppressWarnings("deprecation") + public ChannelFuture register(Channel channel) { + if (channel.getClass().getName().startsWith("org.geysermc.geyser")) { + return super.register(channel); + } + // Starting with Netty 4.2, channels/event loops are very picky with what can be accepted for each. + // For example, IoUringIoHandler (on a Linux machine, what Velocity's worker group will be) + // will not accept LocalChannels on bootstrap creation in GeyserVelocityInjector. + // And using a MultiThreadEventLoopGroup with LocalIoHandler will throw an error when trying to + // connect to the backend server. + // Inserting ourselves here allows our local channels to use the event loop made for LocalChannels, + // while re-using the settings and style of Velocity. + return this.trueWorkerGroup.register(channel); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da00044c0..ce4d1a3b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,8 +5,7 @@ erosion = "1.1-20240521.000109-3" events = "1.1-SNAPSHOT" jackson = "2.17.0" fastutil = "8.5.15-SNAPSHOT" -netty = "4.1.107.Final" -netty-io-uring = "0.0.25.Final-SNAPSHOT" +netty = "4.2.1.Final" guava = "29.0-jre" gson = "2.3.1" # Provided by Spigot 1.8.8 websocket = "1.5.1" @@ -88,7 +87,7 @@ netty-codec-haproxy = { group = "io.netty", name = "netty-codec-haproxy", versio netty-handler = { group = "io.netty", name = "netty-handler", version.ref = "netty" } netty-transport-native-epoll = { group = "io.netty", name = "netty-transport-native-epoll", version.ref = "netty" } netty-transport-native-kqueue = { group = "io.netty", name = "netty-transport-native-kqueue", version.ref = "netty" } -netty-transport-native-io_uring = { group = "io.netty.incubator", name = "netty-incubator-transport-native-io_uring", version.ref = "netty-io-uring" } +netty-transport-native-io_uring = { group = "io.netty", name = "netty-transport-native-io_uring", version.ref = "netty" } log4j-api = { group = "org.apache.logging.log4j", name = "log4j-api", version.ref = "log4j" } log4j-core = { group = "org.apache.logging.log4j", name = "log4j-core", version.ref = "log4j" }