Skip to content

Commit 4ae4200

Browse files
Copilotbrunoborges
andauthored
Add virtual thread support on Java 21+ via Multi-Release JAR
Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/e74714e4-bb84-4af7-b1ef-44f576c5b37c Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>
1 parent caaf6d6 commit 4ae4200

File tree

13 files changed

+281
-11
lines changed

13 files changed

+281
-11
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A
2424

2525
### Requirements
2626

27-
- Java 17 or later. **JDK 25 recommended**. Selecting JDK 25 enables the use of virtual threads, as shown in the [Quick Start](#quick-start).
27+
- Java 17 or later. **JDK 21+ recommended** for automatic virtual thread support (see [Virtual Threads](#virtual-threads) below). Selecting JDK 25 additionally enables the use of virtual threads for the custom executor, as shown in the [Quick Start](#quick-start).
2828
- GitHub Copilot CLI 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`)
2929

3030
### Maven
@@ -143,6 +143,14 @@ jbang https://github.com/github/copilot-sdk-java/blob/latest/jbang-example.java
143143
- [MCP Servers Integration](https://github.github.io/copilot-sdk-java/latest/mcp.html)
144144
- [Cookbook](src/site/markdown/cookbook/) — Practical recipes for common use cases
145145

146+
## Virtual Threads
147+
148+
When running on **Java 21+**, the SDK automatically uses [virtual threads](https://openjdk.org/jeps/444) for its internal I/O threads (JSON-RPC reader loop, CLI stderr forwarding). This is implemented via a [Multi-Release JAR (JEP 238)](https://openjdk.org/jeps/238) — no configuration or code changes are required on your part.
149+
150+
On Java 17–20, the SDK falls back to standard platform (daemon) threads.
151+
152+
> **Note:** The `ScheduledExecutorService` used for `sendAndWait` timeouts always uses platform threads, because the JDK does not provide a virtual-thread-based scheduled executor.
153+
146154
## Projects Using This SDK
147155

148156
| Project | Description |

jbang-example.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
class CopilotSDK {
1313
public static void main(String[] args) throws Exception {
1414
// Create and start client
15+
// On Java 21+, the SDK automatically uses virtual threads for internal I/O.
1516
try (var client = new CopilotClient()) {
1617
client.start().get();
1718

pom.xml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
<archive>
134134
<manifestEntries>
135135
<Automatic-Module-Name>com.github.copilot.sdk.java</Automatic-Module-Name>
136+
<Multi-Release>true</Multi-Release>
136137
</manifestEntries>
137138
</archive>
138139
</configuration>
@@ -261,6 +262,11 @@
261262
<version>2.44.5</version>
262263
<configuration>
263264
<java>
265+
<includes>
266+
<include>src/main/java/**/*.java</include>
267+
<include>src/main/java21/**/*.java</include>
268+
<include>src/test/java/**/*.java</include>
269+
</includes>
264270
<eclipse>
265271
<version>4.33</version>
266272
</eclipse>
@@ -556,6 +562,30 @@
556562
<properties>
557563
<surefire.jvm.args>-XX:+EnableDynamicAgentLoading</surefire.jvm.args>
558564
</properties>
565+
<build>
566+
<plugins>
567+
<plugin>
568+
<groupId>org.apache.maven.plugins</groupId>
569+
<artifactId>maven-compiler-plugin</artifactId>
570+
<executions>
571+
<execution>
572+
<id>compile-java21</id>
573+
<phase>compile</phase>
574+
<goals>
575+
<goal>compile</goal>
576+
</goals>
577+
<configuration>
578+
<release>21</release>
579+
<compileSourceRoots>
580+
<compileSourceRoot>${project.basedir}/src/main/java21</compileSourceRoot>
581+
</compileSourceRoots>
582+
<multiReleaseOutput>true</multiReleaseOutput>
583+
</configuration>
584+
</execution>
585+
</executions>
586+
</plugin>
587+
</plugins>
588+
</build>
559589
</profile>
560590
<!-- Skip git-clone + npm install of the copilot-sdk test harness -->
561591
<profile>

src/main/java/com/github/copilot/sdk/CliServerManager.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ JsonRpcClient connectToServer(Process process, String tcpHost, Integer tcpPort)
172172
}
173173

174174
private void startStderrReader(Process process) {
175-
var stderrThread = new Thread(() -> {
175+
var stderrThread = ThreadFactoryProvider.newThread(() -> {
176176
try (BufferedReader reader = new BufferedReader(
177177
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
178178
String line;
@@ -186,7 +186,6 @@ private void startStderrReader(Process process) {
186186
LOG.log(Level.FINE, "Error reading stderr", e);
187187
}
188188
}, "cli-stderr-reader");
189-
stderrThread.setDaemon(true);
190189
stderrThread.start();
191190
}
192191

src/main/java/com/github/copilot/sdk/CopilotClient.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@
4444
* provides methods to create and manage conversation sessions. It can either
4545
* spawn a CLI server process or connect to an existing server.
4646
* <p>
47+
* <b>Threading:</b> On Java 21+, the SDK automatically uses virtual threads for
48+
* its internal I/O threads (JSON-RPC reader, CLI stderr forwarding). On Java
49+
* 17–20, standard platform threads are used. The
50+
* {@link java.util.concurrent.ScheduledExecutorService} used for
51+
* {@code sendAndWait} timeouts always uses platform threads because the JDK
52+
* does not provide a virtual-thread-based scheduled executor.
53+
* <p>
4754
* Example usage:
4855
*
4956
* <pre>{@code

src/main/java/com/github/copilot/sdk/CopilotSession.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@
8989
* session data on disk — the conversation can be resumed later via
9090
* {@link CopilotClient#resumeSession}. To permanently delete session data, use
9191
* {@link CopilotClient#deleteSession}.
92+
* <p>
93+
* <b>Threading:</b> The {@link java.util.concurrent.ScheduledExecutorService}
94+
* used for {@code sendAndWait} timeouts always uses platform threads regardless
95+
* of the Java version, because the JDK does not provide a virtual-thread-based
96+
* scheduled executor.
9297
*
9398
* <h2>Example Usage</h2>
9499
*

src/main/java/com/github/copilot/sdk/JsonRpcClient.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import java.util.concurrent.CompletionException;
1616
import java.util.concurrent.ConcurrentHashMap;
1717
import java.util.concurrent.ExecutorService;
18-
import java.util.concurrent.Executors;
1918
import java.util.concurrent.atomic.AtomicLong;
2019
import java.util.function.BiConsumer;
2120
import java.util.logging.Level;
@@ -57,11 +56,7 @@ private JsonRpcClient(InputStream inputStream, OutputStream outputStream, Socket
5756
this.outputStream = outputStream;
5857
this.socket = socket;
5958
this.process = process;
60-
this.readerExecutor = Executors.newSingleThreadExecutor(r -> {
61-
Thread t = new Thread(r, "jsonrpc-reader");
62-
t.setDaemon(true);
63-
return t;
64-
});
59+
this.readerExecutor = ThreadFactoryProvider.newSingleThreadExecutor("jsonrpc-reader");
6560
startReader();
6661
}
6762

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk;
6+
7+
import java.util.concurrent.ExecutorService;
8+
import java.util.concurrent.Executors;
9+
import java.util.logging.Logger;
10+
11+
/**
12+
* Provides thread factories for the SDK's internal thread creation.
13+
* <p>
14+
* On Java 17, this class returns standard platform-thread factories. On Java
15+
* 21+, the multi-release JAR overlay replaces this class with one that returns
16+
* virtual-thread factories, giving the SDK lightweight threads for its
17+
* I/O-bound JSON-RPC communication without any user configuration.
18+
* <p>
19+
* The {@link java.util.concurrent.ScheduledExecutorService} used for
20+
* {@code sendAndWait} timeouts in {@link CopilotSession} is <em>not</em>
21+
* affected, because the JDK offers no virtual-thread-based scheduled executor.
22+
*
23+
* @since 0.2.2-java.1
24+
*/
25+
final class ThreadFactoryProvider {
26+
27+
private static final Logger LOG = Logger.getLogger(ThreadFactoryProvider.class.getName());
28+
29+
private ThreadFactoryProvider() {
30+
}
31+
32+
/**
33+
* Creates a new daemon thread with the given name and runnable.
34+
*
35+
* @param runnable
36+
* the task to run
37+
* @param name
38+
* the thread name for debuggability
39+
* @return the new (unstarted) thread
40+
*/
41+
static Thread newThread(Runnable runnable, String name) {
42+
Thread t = new Thread(runnable, name);
43+
t.setDaemon(true);
44+
return t;
45+
}
46+
47+
/**
48+
* Creates a single-thread executor suitable for the JSON-RPC reader loop.
49+
*
50+
* @param name
51+
* the thread name for debuggability
52+
* @return a single-thread {@link ExecutorService}
53+
*/
54+
static ExecutorService newSingleThreadExecutor(String name) {
55+
return Executors.newSingleThreadExecutor(r -> {
56+
Thread t = new Thread(r, name);
57+
t.setDaemon(true);
58+
return t;
59+
});
60+
}
61+
62+
/**
63+
* Returns {@code true} when this class uses virtual threads (Java 21+
64+
* multi-release overlay), {@code false} for platform threads.
65+
*
66+
* @return whether virtual threads are in use
67+
*/
68+
static boolean isVirtualThreads() {
69+
return false;
70+
}
71+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk;
6+
7+
import java.util.concurrent.ExecutorService;
8+
import java.util.concurrent.Executors;
9+
import java.util.logging.Logger;
10+
11+
/**
12+
* Java 21+ override that uses virtual threads for the SDK's internal thread
13+
* creation.
14+
* <p>
15+
* This class is placed under {@code META-INF/versions/21/} in the multi-release
16+
* JAR and replaces the baseline {@code ThreadFactoryProvider} when running on
17+
* Java 21 or later.
18+
*
19+
* @since 0.2.2-java.1
20+
*/
21+
final class ThreadFactoryProvider {
22+
23+
private static final Logger LOG = Logger.getLogger(ThreadFactoryProvider.class.getName());
24+
25+
private ThreadFactoryProvider() {
26+
}
27+
28+
/**
29+
* Creates a new virtual thread with the given name and runnable.
30+
*
31+
* @param runnable
32+
* the task to run
33+
* @param name
34+
* the thread name for debuggability
35+
* @return the new (unstarted) virtual thread
36+
*/
37+
static Thread newThread(Runnable runnable, String name) {
38+
return Thread.ofVirtual().name(name).unstarted(runnable);
39+
}
40+
41+
/**
42+
* Creates a virtual-thread-per-task executor for the JSON-RPC reader loop.
43+
*
44+
* @param name
45+
* the thread name prefix for debuggability
46+
* @return a virtual-thread {@link ExecutorService}
47+
*/
48+
static ExecutorService newSingleThreadExecutor(String name) {
49+
return Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name(name).factory());
50+
}
51+
52+
/**
53+
* Returns {@code true} — this is the virtual-thread overlay.
54+
*
55+
* @return {@code true}
56+
*/
57+
static boolean isVirtualThreads() {
58+
return true;
59+
}
60+
}

src/site/markdown/advanced.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ This guide covers advanced scenarios for extending and customizing your Copilot
5454
- [Session Capabilities](#Session_Capabilities)
5555
- [Outgoing Elicitation via session.getUi()](#Outgoing_Elicitation_via_session.getUi)
5656
- [Getting Session Metadata by ID](#Getting_Session_Metadata_by_ID)
57+
- [Virtual Threads (Java 21+)](#Virtual_Threads_Java_21)
5758

5859
---
5960

@@ -1237,6 +1238,37 @@ This is more efficient than `listSessions()` when you already know the session I
12371238

12381239
---
12391240

1241+
## Virtual Threads (Java 21+)
1242+
1243+
When running on **Java 21 or later**, the SDK automatically uses [virtual threads (JEP 444)](https://openjdk.org/jeps/444) for its internal I/O threads. This is implemented via a [Multi-Release JAR (JEP 238)](https://openjdk.org/jeps/238) — no configuration or code changes are required.
1244+
1245+
### What Uses Virtual Threads
1246+
1247+
| Component | Thread name | Java 17–20 | Java 21+ |
1248+
|-----------|------------|-------------|----------|
1249+
| JSON-RPC reader loop | `jsonrpc-reader` | Platform (daemon) | Virtual |
1250+
| CLI stderr forwarding | `cli-stderr-reader` | Platform (daemon) | Virtual |
1251+
| `sendAndWait` timeouts | `sendAndWait-timeout` | Platform (daemon) | Platform (daemon) |
1252+
1253+
The `sendAndWait` timeout scheduler always uses platform threads because the JDK does not provide a virtual-thread-based `ScheduledExecutorService`.
1254+
1255+
### Performance Implications
1256+
1257+
Virtual threads are lightweight and scheduled by the JVM on a shared `ForkJoinPool` carrier pool. For the I/O-bound JSON-RPC communication this SDK performs, virtual threads reduce memory footprint and improve scalability when many concurrent sessions are active.
1258+
1259+
### How It Works
1260+
1261+
The SDK JAR includes `Multi-Release: true` in its manifest. On Java 21+, the JVM loads the `ThreadFactoryProvider` class from `META-INF/versions/21/`, which uses `Thread.ofVirtual()`. On earlier JVMs, the baseline class under the main class path is loaded, which creates standard platform threads.
1262+
1263+
### Verifying Virtual Thread Usage
1264+
1265+
You can check at runtime whether the SDK is using virtual threads:
1266+
1267+
```java
1268+
// Thread names are preserved for debuggability regardless of thread type.
1269+
// On Java 21+, the jsonrpc-reader and cli-stderr-reader threads will be virtual.
1270+
```
1271+
12401272
## Next Steps
12411273

12421274
- 📖 **[Documentation](documentation.html)** - Core concepts, events, streaming, models, tool filtering, reasoning effort

0 commit comments

Comments
 (0)