diff --git a/README.md b/README.md index 677389c6e..65e1bf6c3 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A ### Requirements -- Java 17 or later. **JDK 25 recommended**. Selecting JDK 25 enables the use of virtual threads, as shown in the [Quick Start](#quick-start). +- Java 17 or later. **JDK 21+ recommended** — the SDK automatically uses virtual threads for its internal I/O on Java 21+, and any virtual-thread-based custom executor examples using `Executors.newVirtualThreadPerTaskExecutor()` also require Java 21+ (see [Virtual Threads](#virtual-threads)). - GitHub Copilot CLI 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`) ### Maven @@ -69,23 +69,17 @@ implementation 'com.github:copilot-sdk-java:0.2.1-java.1' import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.events.SessionUsageInfoEvent; -import com.github.copilot.sdk.json.CopilotClientOptions; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; -import java.util.concurrent.Executors; - public class CopilotSDK { public static void main(String[] args) throws Exception { var lastMessage = new String[]{null}; // Create and start client - try (var client = new CopilotClient()) { // JDK 25+: comment out this line - // JDK 25+: uncomment the following 3 lines for virtual thread support - // var options = new CopilotClientOptions() - // .setExecutor(Executors.newVirtualThreadPerTaskExecutor()); - // try (var client = new CopilotClient(options)) { + // On Java 21+, the SDK automatically uses virtual threads for internal I/O. + try (var client = new CopilotClient()) { client.start().get(); // Create a session @@ -143,6 +137,14 @@ jbang https://github.com/github/copilot-sdk-java/blob/latest/jbang-example.java - [MCP Servers Integration](https://github.github.io/copilot-sdk-java/latest/mcp.html) - [Cookbook](src/site/markdown/cookbook/) — Practical recipes for common use cases +## Virtual Threads + +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. + +On Java 17–20, the SDK falls back to standard platform (daemon) threads. + +> **Note:** The `ScheduledExecutorService` used for `sendAndWait` timeouts always uses platform threads, because the JDK does not provide a virtual-thread-based scheduled executor. + ## Projects Using This SDK | Project | Description | diff --git a/jbang-example.java b/jbang-example.java index 3d02653c1..3d696caa3 100644 --- a/jbang-example.java +++ b/jbang-example.java @@ -12,6 +12,7 @@ class CopilotSDK { public static void main(String[] args) throws Exception { // Create and start client + // On Java 21+, the SDK automatically uses virtual threads for internal I/O. try (var client = new CopilotClient()) { client.start().get(); diff --git a/pom.xml b/pom.xml index 43e36ec4d..57c2a33e8 100644 --- a/pom.xml +++ b/pom.xml @@ -133,6 +133,7 @@ com.github.copilot.sdk.java + true @@ -261,6 +262,11 @@ 2.44.5 + + src/main/java/**/*.java + src/main/java21/**/*.java + src/test/java/**/*.java + 4.33 @@ -556,6 +562,30 @@ -XX:+EnableDynamicAgentLoading + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile-java21 + compile + + compile + + + 21 + + ${project.basedir}/src/main/java21 + + true + + + + + + diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java index 217699986..42172e60d 100644 --- a/src/main/java/com/github/copilot/sdk/CliServerManager.java +++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -172,7 +172,7 @@ JsonRpcClient connectToServer(Process process, String tcpHost, Integer tcpPort) } private void startStderrReader(Process process) { - var stderrThread = new Thread(() -> { + var stderrThread = ThreadFactoryProvider.newThread(() -> { try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { String line; @@ -186,7 +186,6 @@ private void startStderrReader(Process process) { LOG.log(Level.FINE, "Error reading stderr", e); } }, "cli-stderr-reader"); - stderrThread.setDaemon(true); stderrThread.start(); } diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java index f00e2fd11..4dc7fd6c2 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotClient.java +++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -44,6 +44,13 @@ * provides methods to create and manage conversation sessions. It can either * spawn a CLI server process or connect to an existing server. *

+ * Threading: On Java 21+, the SDK automatically uses virtual threads for + * its internal I/O threads (JSON-RPC reader, CLI stderr forwarding). On Java + * 17–20, standard platform threads are used. The + * {@link java.util.concurrent.ScheduledExecutorService} used for + * {@code sendAndWait} timeouts always uses platform threads because the JDK + * does not provide a virtual-thread-based scheduled executor. + *

* Example usage: * *

{@code
diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java
index 23b1b5368..f35353e4c 100644
--- a/src/main/java/com/github/copilot/sdk/CopilotSession.java
+++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java
@@ -89,6 +89,11 @@
  * session data on disk — the conversation can be resumed later via
  * {@link CopilotClient#resumeSession}. To permanently delete session data, use
  * {@link CopilotClient#deleteSession}.
+ * 

+ * Threading: The {@link java.util.concurrent.ScheduledExecutorService} + * used for {@code sendAndWait} timeouts always uses platform threads regardless + * of the Java version, because the JDK does not provide a virtual-thread-based + * scheduled executor. * *

Example Usage

* diff --git a/src/main/java/com/github/copilot/sdk/JsonRpcClient.java b/src/main/java/com/github/copilot/sdk/JsonRpcClient.java index 66ab1726d..4d16b02d3 100644 --- a/src/main/java/com/github/copilot/sdk/JsonRpcClient.java +++ b/src/main/java/com/github/copilot/sdk/JsonRpcClient.java @@ -15,7 +15,6 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.logging.Level; @@ -57,11 +56,7 @@ private JsonRpcClient(InputStream inputStream, OutputStream outputStream, Socket this.outputStream = outputStream; this.socket = socket; this.process = process; - this.readerExecutor = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r, "jsonrpc-reader"); - t.setDaemon(true); - return t; - }); + this.readerExecutor = ThreadFactoryProvider.newSingleThreadExecutor("jsonrpc-reader"); startReader(); } diff --git a/src/main/java/com/github/copilot/sdk/ThreadFactoryProvider.java b/src/main/java/com/github/copilot/sdk/ThreadFactoryProvider.java new file mode 100644 index 000000000..2f864d921 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/ThreadFactoryProvider.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Provides thread factories for the SDK's internal thread creation. + *

+ * On Java 17, this class returns standard platform-thread factories. On Java + * 21+, the multi-release JAR overlay replaces this class with one that returns + * virtual-thread factories, giving the SDK lightweight threads for its + * I/O-bound JSON-RPC communication without any user configuration. + *

+ * The {@link java.util.concurrent.ScheduledExecutorService} used for + * {@code sendAndWait} timeouts in {@link CopilotSession} is not + * affected, because the JDK offers no virtual-thread-based scheduled executor. + * + * @since 0.2.2-java.1 + */ +final class ThreadFactoryProvider { + + private ThreadFactoryProvider() { + } + + /** + * Creates a new daemon thread with the given name and runnable. + * + * @param runnable + * the task to run + * @param name + * the thread name for debuggability + * @return the new (unstarted) thread + */ + static Thread newThread(Runnable runnable, String name) { + Thread t = new Thread(runnable, name); + t.setDaemon(true); + return t; + } + + /** + * Creates a single-thread executor suitable for the JSON-RPC reader loop. + * + * @param name + * the thread name for debuggability + * @return a single-thread {@link ExecutorService} + */ + static ExecutorService newSingleThreadExecutor(String name) { + return Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, name); + t.setDaemon(true); + return t; + }); + } + + /** + * Returns {@code true} when this class uses virtual threads (Java 21+ + * multi-release overlay), {@code false} for platform threads. + * + * @return whether virtual threads are in use + */ + static boolean isVirtualThreads() { + return false; + } +} diff --git a/src/main/java21/com/github/copilot/sdk/ThreadFactoryProvider.java b/src/main/java21/com/github/copilot/sdk/ThreadFactoryProvider.java new file mode 100644 index 000000000..6fef02aca --- /dev/null +++ b/src/main/java21/com/github/copilot/sdk/ThreadFactoryProvider.java @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Java 21+ override that uses virtual threads for the SDK's internal thread + * creation. + *

+ * This class is placed under {@code META-INF/versions/21/} in the multi-release + * JAR and replaces the baseline {@code ThreadFactoryProvider} when running on + * Java 21 or later. + * + * @since 0.2.2-java.1 + */ +final class ThreadFactoryProvider { + + private ThreadFactoryProvider() { + } + + /** + * Creates a new virtual thread with the given name and runnable. + * + * @param runnable + * the task to run + * @param name + * the thread name for debuggability + * @return the new (unstarted) virtual thread + */ + static Thread newThread(Runnable runnable, String name) { + return Thread.ofVirtual().name(name).unstarted(runnable); + } + + /** + * Creates a single-thread executor backed by a virtual-thread factory for the + * JSON-RPC reader loop. + * + * @param name + * the thread name for debuggability + * @return a single-thread virtual-thread-backed {@link ExecutorService} + */ + static ExecutorService newSingleThreadExecutor(String name) { + return Executors.newSingleThreadExecutor(Thread.ofVirtual().name(name).factory()); + } + + /** + * Returns {@code true} — this is the virtual-thread overlay. + * + * @return {@code true} + */ + static boolean isVirtualThreads() { + return true; + } +} diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md index 5ae5c8f94..9b1484750 100644 --- a/src/site/markdown/advanced.md +++ b/src/site/markdown/advanced.md @@ -54,6 +54,7 @@ This guide covers advanced scenarios for extending and customizing your Copilot - [Session Capabilities](#Session_Capabilities) - [Outgoing Elicitation via session.getUi()](#Outgoing_Elicitation_via_session.getUi) - [Getting Session Metadata by ID](#Getting_Session_Metadata_by_ID) +- [Virtual Threads (Java 21+)](#Virtual_Threads_Java_21) --- @@ -1237,6 +1238,32 @@ This is more efficient than `listSessions()` when you already know the session I --- +## Virtual Threads (Java 21+) + +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. + +### What Uses Virtual Threads + +| Component | Thread name | Java 17–20 | Java 21+ | +|-----------|------------|-------------|----------| +| JSON-RPC reader loop | `jsonrpc-reader` | Platform (daemon) | Virtual | +| CLI stderr forwarding | `cli-stderr-reader` | Platform (daemon) | Virtual | +| `sendAndWait` timeouts | `sendAndWait-timeout` | Platform (daemon) | Platform (daemon) | + +The `sendAndWait` timeout scheduler always uses platform threads because the JDK does not provide a virtual-thread-based `ScheduledExecutorService`. + +### Performance Implications + +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. + +### How It Works + +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. + +### Verifying Virtual Thread Usage + +Thread names are preserved for debuggability regardless of thread type. On Java 21+, a thread dump will show `jsonrpc-reader` and `cli-stderr-reader` as virtual threads rather than platform threads. You can verify this via `jcmd Thread.dump_to_file -format=json ` or your IDE's thread inspector. + ## Next Steps - 📖 **[Documentation](documentation.html)** - Core concepts, events, streaming, models, tool filtering, reasoning effort diff --git a/src/site/markdown/getting-started.md b/src/site/markdown/getting-started.md index 39bf43844..913ae7ad2 100644 --- a/src/site/markdown/getting-started.md +++ b/src/site/markdown/getting-started.md @@ -18,7 +18,7 @@ Copilot: In Tokyo it's 75°F and sunny. Great day to be outside! Before you begin, make sure you have: -- **Java 17+** installed +- **Java 17+** installed (**Java 21+ recommended** for automatic [virtual thread](https://openjdk.org/jeps/444) support) - **GitHub Copilot CLI** installed and authenticated ([Installation guide](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)) Verify the CLI is working: diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index b599484d9..1abd0efc1 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -8,7 +8,7 @@ Welcome to the documentation for the **GitHub Copilot SDK for Java** — a Java ### Requirements -- Java 17 or later +- Java 17 or later (**Java 21+ recommended** for automatic [virtual thread](https://openjdk.org/jeps/444) support) - GitHub Copilot CLI 1.0.17 or later installed and in PATH (or provide custom `cliPath`) ### Installation diff --git a/src/test/java/com/github/copilot/sdk/ThreadFactoryProviderTest.java b/src/test/java/com/github/copilot/sdk/ThreadFactoryProviderTest.java new file mode 100644 index 000000000..3ea57ce96 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/ThreadFactoryProviderTest.java @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link ThreadFactoryProvider}, verifying that the factory methods + * produce working threads and executors regardless of the Java version. + */ +class ThreadFactoryProviderTest { + + @Test + void newThreadCreatesNamedThread() { + var ran = new AtomicReference(); + Thread t = ThreadFactoryProvider.newThread(() -> ran.set(Thread.currentThread().getName()), "test-thread"); + assertNotNull(t); + assertEquals("test-thread", t.getName()); + t.start(); + try { + t.join(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + assertEquals("test-thread", ran.get()); + } + + @Test + void newSingleThreadExecutorRunsTask() throws Exception { + ExecutorService executor = ThreadFactoryProvider.newSingleThreadExecutor("test-executor"); + try { + var ran = new AtomicReference(false); + executor.submit(() -> ran.set(true)).get(5, TimeUnit.SECONDS); + assertTrue(ran.get()); + } finally { + executor.shutdownNow(); + } + } + + @Test + void isVirtualThreadsReturnsBoolean() { + // Unit tests run against exploded classes rather than the packaged + // multi-release JAR, so Java 21+ may still load the base implementation + // and report false here. Verify only behavior that does not depend on + // multi-release class selection. + boolean result = ThreadFactoryProvider.isVirtualThreads(); + int javaVersion = Runtime.version().feature(); + if (javaVersion < 21) { + assertFalse(result, "Expected platform threads on Java < 21"); + } else if (result) { + assertTrue(javaVersion >= 21, "Virtual threads are only supported on Java 21+"); + } + } +}