Netty
Netty is a networking library built around non-blocking I/O and the event loop model.
In Kora it is a low-level network transport for modules that have to process connections and network events efficiently.
The module provides no user-facing API of its own.
It exists so that every Netty-based part of an application shares one event loop and one transport decision,
instead of each of them starting its own I/O threads.
The Redis cache driver is built on it, and a custom Netty transport written inside a service can be built on the very same components.
These settings are useful when an application has to control the network transport, the number of I/O threads,
or the choice of a native transport.
The defaults suit most services, but they can be set explicitly under high network load or specific environment requirements.
The module configures the Netty transport and the Netty event loop within Kora.
Connection¶
Usually the module does not need to be connected manually: Kora modules that require Netty extend NettyModule themselves
and bring it as a transitive dependency.
The Redis cache module is exactly such a case: LettuceRedisCacheModule extends LettuceModule,
and LettuceModule extends NettyModule.
Connect it explicitly only when a custom Netty transport is built inside the service and should share the event loop with everything else:
Dependency build.gradle:
Module:
Dependency build.gradle.kts:
Module:
What it provides¶
NettyModule contributes the following shared components to the dependency container:
NettyTransportConfig- configuration bound to thenettysection: the preferred transport and the thread count of theevent loopgroups.- Worker
EventLoopGroupwith tag@Tag(NettyModule.EventLoopWorker.class)- the sharedevent loopthat processes established connections and network I/O. Netty-based clients run on it. - Boss
EventLoopGroupwith tag@Tag(NettyModule.EventLoopBoss.class)- a separateevent loopmeant for serverbootstrapsthat accept incoming connections, so that accepting a connection is never delayed by I/O work on established ones. NettyEventLoopFactorywith the same two tags - the factories the groups above are built from.NettyChannelFactory- a factory that creates Netty channels matching the selected transport.
Both groups are built from the same transport selection and are sized by the same threads parameter,
so worker and boss threads are always of the same kind.
Both event loop groups are managed by the Kora lifecycle:
they are shut down gracefully after every component that depends on them has been released, so no manual management is required.
The event loop factories, both groups and NettyChannelFactory are registered as standard components,
so any of them can be replaced by declaring a component of the same type with the same tag.
Components that build their own Netty transport inject these components directly:
@Component
public final class MyNettyTransport {
private final Bootstrap bootstrap;
public MyNettyTransport(@Tag(NettyModule.EventLoopWorker.class) EventLoopGroup workerGroup,
NettyChannelFactory channelFactory) {
this.bootstrap = new Bootstrap()
.group(workerGroup)
.channelFactory(channelFactory.build());
}
}
Configuration¶
An example of the configuration described by the NettyTransportConfig interface:
- Preferred transport:
NIO,EPOLL,KQUEUEorURING(optional, when not set the transport is selected automatically). - Number of threads in each
event loopgroup (default: the number of processors available to Netty multiplied by2). The worker and boss groups are sized by this same value.
- Preferred transport:
NIO,EPOLL,KQUEUEorURING(optional, when not set the transport is selected automatically). - Number of threads in each
event loopgroup (default: the number of processors available to Netty multiplied by2). The worker and boss groups are sized by this same value.
The default thread count comes from Netty's own NettyRuntime.availableProcessors(), which falls back to
Runtime.availableProcessors() and can be overridden with the io.netty.availableProcessors system property.
Set that property, or threads explicitly, when the CPU limit of the container does not match what the JVM reports.
The root-level netty section belongs to this module only.
Drivers that embed their own Netty stack configure it under their own section:
the Cassandra driver, for example, has a nested cassandra.advanced.netty section
that configures the driver's own event loop groups and has nothing to do with the shared transport described here.
Transport¶
The transport parameter sets the preferred Netty transport:
NIO- standard Java NIOtransport, always available.EPOLL- Linuxnative transport.KQUEUE- macOS / BSDnative transport.URING- Linuxio_uringnative transport.
If transport is not set, Kora picks the first transport available at runtime, in this order:
URINGEPOLLKQUEUENIO
Availability of a native transport means two things at once: its classes are on the classpath,
and Netty reports the platform as supported.
If the configured native transport fails either check, Kora falls back to the same order instead of failing to start.
NIO is the only value that is always honored exactly as configured, because it has no availability check.
Note that the automatic order prefers io_uring over epoll:
adding the io_uring native transport to the runtime classpath of a Linux service changes the selected transport
even if nothing else in the configuration changed.
Pin transport explicitly when the selection has to stay stable across environments.
The same selection is applied to both event loop groups and to the channel factory,
so channels and the event loop they are registered on always come from the same transport.
Native Transport¶
The module compiles against the native transport classes but does not bring them at runtime,
so to use EPOLL, KQUEUE or URING the corresponding Netty dependency must be present in the runtime classpath:
| Transport | Artifact | Platform | Classifiers |
|---|---|---|---|
EPOLL |
io.netty:netty-transport-native-epoll |
Linux | linux-x86_64, linux-aarch_64, linux-riscv64 |
KQUEUE |
io.netty:netty-transport-native-kqueue |
macOS / BSD | osx-x86_64, osx-aarch_64 |
URING |
io.netty:netty-transport-native-io_uring |
Linux | linux-x86_64, linux-aarch_64, linux-riscv64 |
The classifier must match the target platform, and the version must match the rest of the io.netty artifacts on the classpath.
Kora 2.0 brings Netty 4.2.17.Final:
Dependency build.gradle:
Dependency build.gradle.kts:
The native artifact brings the matching netty-transport-classes-* artifact transitively, so it does not have to be declared separately.
A service built for several platforms declares one native artifact per platform: they coexist on the classpath,
and the availability check picks the one that actually works.
Recommendation
Usually it is enough to leave transport unset and let Kora select it automatically.
Add a native transport intentionally: for example, when it is required for performance or for Netty features unavailable in NIO.
For a GraalVM native image the module ships metadata that initializes the io.netty classes at run time,
which is what native transports require.
Channel factory¶
NettyChannelFactory is a shared injectable component that produces Netty ChannelFactory instances matching the selected transport.
It is the injection point for components that build their own Netty client or server bootstrap and want channels consistent with the chosen transport:
build()/build(boolean domainSocket)- aChannelFactory<Channel>for client channels.buildServer()/buildServer(boolean domainSocket)- aChannelFactory<ServerChannel>for server channels.
The no-argument overloads delegate to the boolean ones with domainSocket = false and create standard TCP socket channels.
Passing domainSocket = true requests a Unix domain socket channel:
every transport, NIO included, provides both a client and a server domain socket channel implementation.
Event loop factory¶
NettyEventLoopFactory is a single-method factory that builds an EventLoopGroup for the selected transport
with the configured thread count.
Kora registers two of them, tagged @Tag(NettyModule.EventLoopWorker.class) and @Tag(NettyModule.EventLoopBoss.class),
and builds the two shared groups from them.
Inject a factory instead of a group when a component needs an isolated EventLoopGroup rather than the shared one -
for example when its I/O must not compete with the rest of the application:
@Component
public final class MyIsolatedTransport implements Lifecycle {
private final EventLoopGroup eventLoopGroup;
public MyIsolatedTransport(@Tag(NettyModule.EventLoopWorker.class) NettyEventLoopFactory factory) {
this.eventLoopGroup = factory.build();
}
@Override
public void init() { }
@Override
public void release() throws Exception {
eventLoopGroup.shutdownGracefully().get();
}
}
A group created this way is not managed by the container, so the component that created it is responsible for shutting it down, as in the example above via the component lifecycle.
Thread factory¶
Each event loop group is built with a ThreadFactory.
By default Kora uses Netty's DefaultThreadFactory
producing daemon threads named after netty-kora-worker for the worker group and netty-kora-boss for the boss group.
To customize thread naming or priority, provide a ThreadFactory component with the tag of the group it belongs to.
The tags are separate, so worker and boss threads can be configured independently,
and providing only one of them leaves the other on the default:
@KoraApp
public interface Application extends LettuceRedisCacheModule {
@Tag(NettyModule.EventLoopWorker.class)
default ThreadFactory nettyWorkerThreadFactory() {
return new DefaultThreadFactory("netty-worker", true);
}
@Tag(NettyModule.EventLoopBoss.class)
default ThreadFactory nettyBossThreadFactory() {
return new DefaultThreadFactory("netty-boss", true);
}
}
@KoraApp
interface Application : LettuceRedisCacheModule {
@Tag(NettyModule.EventLoopWorker::class)
fun nettyWorkerThreadFactory(): ThreadFactory = DefaultThreadFactory("netty-worker", true)
@Tag(NettyModule.EventLoopBoss::class)
fun nettyBossThreadFactory(): ThreadFactory = DefaultThreadFactory("netty-boss", true)
}
Kora's own default thread factories create daemon threads on purpose: the event loop groups are released by the container,
and daemon threads must not keep the JVM alive if a shutdown path is skipped.
A custom ThreadFactory should keep that property.