Skip to content

Commit f4d52e3

Browse files
committed
Add EchoServer example demonstrating VirtualMultithreadIoEventLoopGroup usage
1 parent f656651 commit f4d52e3

6 files changed

Lines changed: 292 additions & 60 deletions

File tree

README.md

Lines changed: 73 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,98 @@
11
# Netty VirtualThread Scheduler
22

33
## Introduction
4-
This project provides an advanced integration between Java Virtual Threads (Project Loom) and Netty's event loop, enabling seamless execution of blocking operations within the Netty event loop itself. Unlike standard approaches, which require offloading blocking tasks to external thread pools (incurring multiple thread hand-offs), this scheduler allows blocking code to run directly on the event loop's carrier thread, leveraging the virtual thread execution model.
4+
This project provides an integration between Java Virtual Threads (Project Loom) and Netty's event loop, enabling low-overhead execution of blocking or CPU-bound work started from the Netty event loop without the usual extra thread hand-offs.
55

6-
## Motivation
7-
In traditional Netty applications, blocking operations (e.g., JDBC, file I/O) must not run on the event loop. The usual workaround is:
8-
1. Offload the blocking task to an external thread pool (often using the default virtual thread scheduler).
9-
2. Once complete, hand control back to the Netty event loop to continue processing (e.g., send a response).
6+
At a high level:
7+
- Each Netty event loop is backed by a virtual thread (the "event loop virtual thread").
8+
- Each such virtual thread is executed on a dedicated carrier platform thread.
9+
- Virtual threads created from the event-loop-specific ThreadFactory returned by the group will be associated with the same EventLoopScheduler and, when possible, will run on the same carrier platform thread as the event loop.
1010

11-
This process involves at least two thread hand-offs, which not only increases latency and complexity, but also wastes CPU cycles due to waking up both the external thread pool and the event loop again. Additionally, moving data between threads harms cache locality, reducing cache friendliness and overall performance.
11+
This allows code that must block (for example, blocking I/O or synchronous library calls) to be executed without moving work between unrelated threads, reducing wake-ups and improving cache locality compared to offloading to an external thread pool.
1212

13-
## Technical Approach
14-
- The Netty event loop runs as a special, long-lived virtual thread.
15-
- Blocking operations issued from the event loop are executed as continuations, scheduled to run on the same platform thread (the "carrier")—unless work-stealing occurs (not implemented at the moment).
16-
- Any virtual thread created from the event loop will, by default, run on the same carrier platform thread (again, unless work-stealing is introduced).
17-
- This enables blocking libraries to be used transparently, without extra thread pools or hand-offs.
13+
## Key behavior (user-facing)
14+
- Create a `VirtualMultithreadIoEventLoopGroup` like any other Netty `EventLoopGroup`. It behaves like a `MultiThreadIoEventLoopGroup` from the Netty API, but the event loops are driven by virtual threads and coordinated with carrier platform threads by a per-event-loop `EventLoopScheduler`.
15+
- Call `group.vThreadFactory()` to obtain a `ThreadFactory` that creates virtual threads tied to an `EventLoopScheduler` of the group. When those virtual threads block, the scheduler parks them and resumes them later using the carrier thread.
16+
- Virtual threads created via the group's `vThreadFactory()` will attempt to inherit the scheduler and run with low-overhead handoffs back to the event loop when continuing work.
17+
- Virtual threads created with `Thread.ofVirtual().factory()` (the JVM default factory) do NOT automatically inherit the group's scheduler.
18+
- The library requires a custom global virtual thread scheduler implementation to be installed: set `-Djdk.virtualThreadScheduler.implClass=io.netty.loom.NettyScheduler` (or use the convenience helpers in the code/tests that set up the scheduler).
19+
- Blocking I/O support that relies on per-carrier pollers currently depends on the JVM poller mode (the code checks `jdk.pollerMode==3`). See notes below.
1820

19-
## Comparison Table
21+
## When to use this
22+
- You have code that must perform blocking operations from a Netty handler and you want to avoid an extra thread hand-off.
23+
- You want fewer CPU wake-ups and better cache locality by keeping related work on the same carrier platform thread.
2024

21-
| Aspect | Standard Netty + Loom (Default) | Netty with VirtualThread Scheduler (This Project) |
22-
|-----------------------|-----------------------------------------|--------------------------------------------------|
23-
| Blocking Operation | Offloaded to external thread pool | Runs as continuation on event loop carrier |
24-
| Thread Hand-offs | 2+ | 0 |
25-
| Cache Friendliness | Poor (data moves between threads) | High (data stays on carrier thread) |
26-
| CPU Wakeups | More (wakes both pools) | Fewer (single carrier thread) |
25+
Caveats:
26+
- This project leverages experimental JVM features (Project Loom) and assumes recent Java versions (Java 21+ recommended).
27+
- You must install the Netty-specific scheduler (see above) for `VirtualMultithreadIoEventLoopGroup` to be usable.
2728

28-
## Architecture Diagrams
29+
## Usage example (simple)
2930

31+
See the runnable example and step-by-step instructions in the example module:
32+
33+
- example-echo/README.md
34+
35+
(That README contains the minimal build and run commands, including how to start the example server and run a quick curl smoke test.)
36+
37+
## About the Loom build used
38+
39+
This work targets Project Loom features and was developed and tested against very recent OpenJDK / Loom builds. If you don't want to build OpenJDK yourself, a convenient set of prebuilt Loom-enabled JDK images is available from Shipilev's builds:
40+
41+
- https://builds.shipilev.net/openjdk-jdk-loom/
42+
43+
Follow that site to download a suitable JDK image for your platform and point `JAVA_HOME` to the JDK image before running the project. Example:
44+
45+
```sh
46+
export JAVA_HOME=/path/to/loom/build/linux-x86_64-server-release/jdk/
3047
```
31-
Standard Netty + Loom (Default):
32-
33-
┌──────────────┐ offload ┌────────────────────────────┐ callback ┌──────────────┐
34-
│ EventLoop │───────────▶│ Virtual Thread (Scheduler) │────────────▶│ EventLoop │
35-
│ (OS Thread) │ │ (External Thread Pool) │ │ (OS Thread) │
36-
└──────────────┘ └────────────────────────────┘ └──────────────┘
37-
38-
Netty with VirtualThread Scheduler:
39-
40-
┌────────────────────────────────────────────┐
41-
│ Platform Thread (Carrier) │
42-
│ ┌──────────────────────────────────────┐ │
43-
│ │ EventLoop (Virtual Thread) │ │
44-
│ │ (runs on carrier platform thread) │ │
45-
│ │ ──────────────────────────────── │ │
46-
│ │ Blocking Operation │ │
47-
│ │ (Continuation, same platform) │ │
48-
│ └──────────────────────────────────────┘ │
49-
└────────────────────────────────────────────┘
48+
49+
If you prefer to build from the OpenJDK `loom` repository yourself, the upstream source is:
50+
51+
- https://github.com/openjdk/loom
52+
53+
If you have a local build of the latest Loom-enabled OpenJDK, point `JAVA_HOME` to that build before running tests and benchmarks. Example:
54+
55+
```sh
56+
export JAVA_HOME=/path/to/your/loom/build/linux-x86_64-server-release/jdk/
57+
mvn clean install
5058
```
5159

52-
## Build and Run
60+
## Integration tips and runtime flags
61+
- Install the Netty scheduler as the JVM virtual-thread scheduler with:
62+
-Djdk.virtualThreadScheduler.implClass=io.netty.loom.NettyScheduler
63+
64+
- Some blocking I/O integrations rely on per-carrier pollers. The code checks `jdk.pollerMode` and expects value `3` for per-carrier pollers. This can be controlled via JVM flags or defaults depending on your JVM version.
65+
66+
- Use `group.vThreadFactory()` from inside Netty handlers if you want the spawned virtual thread to be associated with the same event loop scheduler as the handler's event loop.
5367

68+
- If you need the virtual thread to inherit the scheduler when forking tasks via StructuredTaskScope, pass the group's `vThreadFactory()` to the scope's `withThreadFactory(...)`.
69+
70+
## Build and Run
5471
This project uses Maven for build and dependency management.
5572

56-
1. **Build the project:**
57-
```sh
58-
mvn clean install
59-
```
60-
2. **Run Benchmarks:**
61-
```sh
62-
cd benchmarks
63-
mvn clean install
64-
java -jar target/benchmarks.jar
65-
```
73+
1. Build the project:
74+
75+
```sh
76+
mvn clean install
77+
```
78+
79+
2. Run the benchmarks (optional):
80+
81+
```sh
82+
cd benchmarks
83+
mvn clean install
84+
java -jar target/benchmarks.jar
85+
```
6686

6787
## Prerequisites
6888
- Java 21 or newer (with Loom support)
6989
- Maven 3.6+
90+
- To use the `VirtualMultithreadIoEventLoopGroup` set the JVM property to install the Netty scheduler:
91+
-Djdk.virtualThreadScheduler.implClass=io.netty.loom.NettyScheduler
7092

7193
## References
72-
- [Project Loom (OpenJDK)](https://openjdk.org/projects/loom/)
73-
- [Netty Project](https://netty.io/)
94+
- Project Loom (OpenJDK): https://openjdk.org/projects/loom/
95+
- Netty Project: https://netty.io/
7496

7597
---
7698
For more details, see the source code and benchmark results in the respective modules.

core/src/main/java/io/netty/loom/NettyScheduler.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,25 @@
77
/**
88
* Global Netty scheduler proxy for virtual threads.
99
*
10-
* <p>Inheritance rule (exact): a newly started virtual thread inherits the
10+
* <p>
11+
* Inheritance rule (exact): a newly started virtual thread inherits the
1112
* caller's {@code EventLoopScheduler} only when both conditions are true:
1213
* <ol>
13-
* <li>{@code jdk.pollerMode} is {@code 3} (per-carrier pollers); and</li>
14-
* <li>the thread performing the start/poller I/O is itself running under an
15-
* {@code EventLoopScheduler} (i.e. {@code EventLoopScheduler.currentThreadSchedulerContext().scheduler()}
16-
* returns a non-null {@code SharedRef}).</li>
14+
* <li>{@code jdk.pollerMode} is {@code 3} (per-carrier pollers); and</li>
15+
* <li>the thread performing the start/poller I/O is itself running under an
16+
* {@code EventLoopScheduler} (i.e.
17+
* {@code EventLoopScheduler.currentThreadSchedulerContext().scheduler()}
18+
* returns a non-null {@code SharedRef}).</li>
1719
* </ol>
1820
*
19-
* <p>The current implementation only attempts scheduler inheritance for
20-
* poller-created virtual threads (recognized by the {@code "-Read-Poller"}
21-
* name suffix). If either condition above is not met (or the thread kind is
21+
* <p>
22+
* The current implementation only attempts scheduler inheritance for
23+
* poller-created virtual threads (recognized by the {@code "-Read-Poller"} name
24+
* suffix). If either condition above is not met (or the thread kind is
2225
* unrecognized) the virtual thread falls back to the default JDK scheduler.
2326
*
24-
* <p>This class is a proxy/dispatcher and does not implement a standalone
27+
* <p>
28+
* This class is a proxy/dispatcher and does not implement a standalone
2529
* scheduling policy. See {@link EventLoopScheduler} for details about scheduler
2630
* attachment and execution.
2731
*/

example-echo/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# example-echo
2+
3+
A tiny HTTP example that shows how to use VirtualMultithreadIoEventLoopGroup and its
4+
`vThreadFactory()` to spawn virtual threads from Netty handlers.
5+
6+
Prerequisites
7+
- A recent Loom-enabled JDK (set `JAVA_HOME` to it).
8+
- Maven (for build and runtime classpath).
9+
- curl for the smoke test; jbang if you want to run the optional wrk/Hyperfoil test.
10+
11+
How to build and run (minimal)
12+
13+
1) Build (from repository root):
14+
15+
```bash
16+
# build quickly (skip tests)
17+
mvn -DskipTests package
18+
```
19+
20+
2) Start the server (adjust `JAVA_HOME`):
21+
22+
```bash
23+
# compute runtime classpath for example-echo
24+
CP_LINE=$(mvn -q -pl example-echo dependency:build-classpath -DincludeScope=runtime -DskipTests | tail -n1)
25+
CP="core/target/netty-virtualthread-core-1.0-SNAPSHOT.jar:example-echo/target/example-echo-1.0-SNAPSHOT.jar:${CP_LINE}"
26+
27+
# start server in background (use your Loom JDK)
28+
export JAVA_HOME=/path/to/loom/build/linux-x86_64-server-release/jdk/
29+
"$JAVA_HOME/bin/java" --enable-preview \
30+
-Djdk.virtualThreadScheduler.implClass=io.netty.loom.NettyScheduler \
31+
-Djdk.pollerMode=3 \
32+
--add-opens=java.base/java.lang=ALL-UNNAMED \
33+
-cp "$CP" io.netty.loom.example.EchoServer &
34+
35+
# note the PID in $!
36+
```
37+
38+
Quick smoke & load tests
39+
40+
- Simple curl smoke test:
41+
42+
```bash
43+
curl -v http://localhost:8080/
44+
```
45+
46+
- Short wrk/Hyperfoil test via jbang (uses the Hyperfoil catalog):
47+
48+
```bash
49+
# short 5s test
50+
jbang wrk@hyperfoil -t1 -c10 -d5s -R10 --latency http://localhost:8080/
51+
```
52+
53+
Notes
54+
- `-Djdk.pollerMode=3` enables per-carrier pollers (useful for blocking I/O tests). Remove or change if you don't need it.
55+
- Use a Loom-enabled JDK and set `JAVA_HOME` accordingly.
56+
- If anything fails, paste the exact output here and I'll help debug.

example-echo/pom.xml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>io.netty.loom</groupId>
8+
<artifactId>netty-virtualthread-parent</artifactId>
9+
<version>1.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>example-echo</artifactId>
13+
<packaging>jar</packaging>
14+
15+
<dependencies>
16+
<dependency>
17+
<groupId>io.netty.loom</groupId>
18+
<artifactId>netty-virtualthread-core</artifactId>
19+
<version>${project.version}</version>
20+
</dependency>
21+
<dependency>
22+
<groupId>io.netty</groupId>
23+
<artifactId>netty-all</artifactId>
24+
</dependency>
25+
</dependencies>
26+
27+
<build>
28+
<plugins>
29+
<plugin>
30+
<groupId>org.apache.maven.plugins</groupId>
31+
<artifactId>maven-compiler-plugin</artifactId>
32+
</plugin>
33+
<plugin>
34+
<groupId>org.codehaus.mojo</groupId>
35+
<artifactId>exec-maven-plugin</artifactId>
36+
<version>3.1.0</version>
37+
<configuration>
38+
<mainClass>io.netty.loom.example.EchoServer</mainClass>
39+
<arguments/>
40+
<systemProperties>
41+
<systemProperty>
42+
<key>jdk.virtualThreadScheduler.implClass</key>
43+
<value>io.netty.loom.NettyScheduler</value>
44+
</systemProperty>
45+
</systemProperties>
46+
<cleanupDaemonThreads>false</cleanupDaemonThreads>
47+
</configuration>
48+
</plugin>
49+
50+
<!-- Shade plugin to produce an executable fat jar -->
51+
<plugin>
52+
<groupId>org.apache.maven.plugins</groupId>
53+
<artifactId>maven-shade-plugin</artifactId>
54+
<version>3.5.0</version>
55+
<executions>
56+
<execution>
57+
<phase>package</phase>
58+
<goals>
59+
<goal>shade</goal>
60+
</goals>
61+
<configuration>
62+
<createDependencyReducedPom>false</createDependencyReducedPom>
63+
<transformers>
64+
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
65+
<mainClass>io.netty.loom.example.EchoServer</mainClass>
66+
</transformer>
67+
</transformers>
68+
</configuration>
69+
</execution>
70+
</executions>
71+
</plugin>
72+
73+
</plugins>
74+
</build>
75+
</project>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.netty.loom.example;
2+
3+
import io.netty.bootstrap.ServerBootstrap;
4+
import io.netty.channel.Channel;
5+
import io.netty.channel.ChannelInboundHandlerAdapter;
6+
import io.netty.channel.ChannelInitializer;
7+
import io.netty.channel.nio.NioIoHandler;
8+
import io.netty.channel.socket.SocketChannel;
9+
import io.netty.channel.socket.nio.NioServerSocketChannel;
10+
import io.netty.handler.codec.http.DefaultFullHttpResponse;
11+
import io.netty.handler.codec.http.DefaultHttpRequest;
12+
import io.netty.handler.codec.http.HttpHeaderNames;
13+
import io.netty.handler.codec.http.HttpHeaderValues;
14+
import io.netty.handler.codec.http.HttpResponseStatus;
15+
import io.netty.handler.codec.http.HttpServerCodec;
16+
import io.netty.handler.codec.http.HttpVersion;
17+
import io.netty.util.CharsetUtil;
18+
import io.netty.util.ReferenceCountUtil;
19+
import io.netty.loom.VirtualMultithreadIoEventLoopGroup;
20+
21+
import java.util.concurrent.ThreadFactory;
22+
23+
public class EchoServer {
24+
25+
public static void main(String[] args) throws Exception {
26+
// simple echo server on port 8080
27+
try (var group = new VirtualMultithreadIoEventLoopGroup(1, NioIoHandler.newFactory())) {
28+
var bootstrap = new ServerBootstrap().group(group).channel(NioServerSocketChannel.class)
29+
.childHandler(new ChannelInitializer<SocketChannel>() {
30+
@Override
31+
protected void initChannel(SocketChannel ch) {
32+
ch.pipeline().addLast(new HttpServerCodec());
33+
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
34+
@Override
35+
public void channelRead(io.netty.channel.ChannelHandlerContext ctx, Object msg) {
36+
if (msg instanceof DefaultHttpRequest) {
37+
ThreadFactory vtf = group.vThreadFactory();
38+
vtf.newThread(() -> {
39+
try {
40+
// blocking work placeholder
41+
Thread.sleep(50);
42+
var content = ctx.alloc().directBuffer("HELLO".length());
43+
content.writeCharSequence("HELLO", CharsetUtil.US_ASCII);
44+
var response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
45+
HttpResponseStatus.OK, content);
46+
response.headers()
47+
.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN)
48+
.set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes())
49+
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
50+
// write the response; the inbound request is not needed by the virtual
51+
// thread, so it is safe to release it immediately after scheduling.
52+
ctx.writeAndFlush(response);
53+
} catch (InterruptedException e) {
54+
Thread.currentThread().interrupt();
55+
}
56+
}).start();
57+
// release the inbound message immediately: the virtual thread does not use it
58+
ReferenceCountUtil.release(msg);
59+
} else {
60+
ReferenceCountUtil.release(msg);
61+
}
62+
}
63+
});
64+
}
65+
});
66+
67+
Channel ch = bootstrap.bind(8080).sync().channel();
68+
System.out.println("Echo server started on http://localhost:8080/");
69+
70+
// keep server running until the channel is closed (by external shutdown)
71+
ch.closeFuture().sync();
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)