Skip to content

Commit b9ad1c6

Browse files
brunoborgesCopilot
andcommitted
feat(java): add JDK 25 default executor
Use a multi-release JAR to select virtual threads as the default internal executor on JDK 25+, while retaining the common pool on older JDKs. Keep user-provided executors caller-owned and only shut down SDK-owned defaults. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6010405 commit b9ad1c6

7 files changed

Lines changed: 252 additions & 35 deletions

File tree

java/README.md

Lines changed: 2 additions & 10 deletions
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 25 recommended**. On JDK 25 and later, the SDK automatically uses virtual threads for its default internal executor.
2828
- GitHub Copilot CLI 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`)
2929

3030
### Maven
@@ -69,23 +69,16 @@ implementation 'com.github:copilot-sdk-java:1.0.0-beta-java.4'
6969
import com.github.copilot.CopilotClient;
7070
import com.github.copilot.generated.AssistantMessageEvent;
7171
import com.github.copilot.generated.SessionUsageInfoEvent;
72-
import com.github.copilot.rpc.CopilotClientOptions;
7372
import com.github.copilot.rpc.MessageOptions;
7473
import com.github.copilot.rpc.PermissionHandler;
7574
import com.github.copilot.rpc.SessionConfig;
7675

77-
import java.util.concurrent.Executors;
78-
7976
public class CopilotSDK {
8077
public static void main(String[] args) throws Exception {
8178
var lastMessage = new String[]{null};
8279

8380
// Create and start client
84-
try (var client = new CopilotClient()) { // JDK 25+: comment out this line
85-
// JDK 25+: uncomment the following 3 lines for virtual thread support
86-
// var options = new CopilotClientOptions()
87-
// .setExecutor(Executors.newVirtualThreadPerTaskExecutor());
88-
// try (var client = new CopilotClient(options)) {
81+
try (var client = new CopilotClient()) {
8982
client.start().get();
9083

9184
// Create a session
@@ -212,4 +205,3 @@ MIT — see [LICENSE](LICENSE) for details.
212205
[![Star History Chart](https://api.star-history.com/svg?repos=github/copilot-sdk-java&type=Date)](https://www.star-history.com/#github/copilot-sdk-java&Date)
213206

214207
⭐ Drop a star if you find this useful!
215-

java/pom.xml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,10 @@
447447
<configuration>
448448
<dataFile>${project.build.directory}/jacoco-test-results/sdk-tests.exec</dataFile>
449449
<outputDirectory>${project.reporting.outputDirectory}/jacoco-coverage</outputDirectory>
450+
<excludes>
451+
<!-- Exclude multi-release classes to avoid duplicate class analysis. -->
452+
<exclude>META-INF/versions/**/*.class</exclude>
453+
</excludes>
450454
</configuration>
451455
</execution>
452456
</executions>
@@ -507,6 +511,48 @@
507511
<surefire.jvm.args>-XX:+EnableDynamicAgentLoading</surefire.jvm.args>
508512
</properties>
509513
</profile>
514+
<profile>
515+
<id>java25-multi-release</id>
516+
<activation>
517+
<jdk>[25,)</jdk>
518+
</activation>
519+
<build>
520+
<plugins>
521+
<plugin>
522+
<groupId>org.apache.maven.plugins</groupId>
523+
<artifactId>maven-compiler-plugin</artifactId>
524+
<executions>
525+
<execution>
526+
<id>compile-java25</id>
527+
<phase>compile</phase>
528+
<goals>
529+
<goal>compile</goal>
530+
</goals>
531+
<configuration>
532+
<release>25</release>
533+
<useIncrementalCompilation>false</useIncrementalCompilation>
534+
<compileSourceRoots>
535+
<compileSourceRoot>${project.basedir}/src/main/java25</compileSourceRoot>
536+
</compileSourceRoots>
537+
<multiReleaseOutput>true</multiReleaseOutput>
538+
</configuration>
539+
</execution>
540+
</executions>
541+
</plugin>
542+
<plugin>
543+
<groupId>org.apache.maven.plugins</groupId>
544+
<artifactId>maven-jar-plugin</artifactId>
545+
<configuration>
546+
<archive>
547+
<manifestEntries>
548+
<Multi-Release>true</Multi-Release>
549+
</manifestEntries>
550+
</archive>
551+
</configuration>
552+
</plugin>
553+
</plugins>
554+
</build>
555+
</profile>
510556
<!-- Skip git-clone + npm install of the copilot-sdk test harness -->
511557
<profile>
512558
<id>skip-test-harness</id>

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

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.concurrent.CompletionException;
1515
import java.util.concurrent.ConcurrentHashMap;
1616
import java.util.concurrent.Executor;
17+
import java.util.concurrent.ExecutorService;
1718
import java.util.concurrent.RejectedExecutionException;
1819
import java.util.concurrent.TimeUnit;
1920
import java.util.logging.Level;
@@ -78,6 +79,8 @@ public final class CopilotClient implements AutoCloseable {
7879
public static final int AUTOCLOSEABLE_TIMEOUT_SECONDS = 10;
7980
private static final int FORCE_KILL_TIMEOUT_SECONDS = 10;
8081
private final CopilotClientOptions options;
82+
private final Executor executor;
83+
private final ExecutorService ownedExecutor;
8184
private final CliServerManager serverManager;
8285
private final LifecycleEventManager lifecycleManager = new LifecycleEventManager();
8386
private final Map<String, CopilotSession> sessions = new ConcurrentHashMap<>();
@@ -153,6 +156,11 @@ public CopilotClient(CopilotClientOptions options) {
153156
this.optionsPort = null;
154157
}
155158

159+
Executor providedExecutor = this.options.getExecutor();
160+
this.executor = providedExecutor != null ? providedExecutor : DefaultExecutorProvider.create();
161+
this.ownedExecutor = providedExecutor == null && DefaultExecutorProvider.isOwned(this.executor)
162+
&& this.executor instanceof ExecutorService executorService ? executorService : null;
163+
156164
this.serverManager = new CliServerManager(this.options);
157165
this.serverManager.setConnectionToken(this.effectiveConnectionToken);
158166
}
@@ -176,11 +184,8 @@ public CompletableFuture<Void> start() {
176184
private CompletableFuture<Connection> startCore() {
177185
LOG.fine("Starting Copilot client");
178186

179-
Executor exec = options.getExecutor();
180187
try {
181-
return exec != null
182-
? CompletableFuture.supplyAsync(this::startCoreBody, exec)
183-
: CompletableFuture.supplyAsync(this::startCoreBody);
188+
return CompletableFuture.supplyAsync(this::startCoreBody, executor);
184189
} catch (RejectedExecutionException e) {
185190
return CompletableFuture.failedFuture(e);
186191
}
@@ -209,8 +214,7 @@ private Connection startCoreBody() {
209214
Connection connection = new Connection(rpc, process, new ServerRpc(rpc::invoke));
210215

211216
// Register handlers for server-to-client calls
212-
RpcHandlerDispatcher dispatcher = new RpcHandlerDispatcher(sessions, lifecycleManager::dispatch,
213-
options.getExecutor());
217+
RpcHandlerDispatcher dispatcher = new RpcHandlerDispatcher(sessions, lifecycleManager::dispatch, executor);
214218
dispatcher.registerHandlers(rpc);
215219

216220
// Verify protocol version
@@ -308,7 +312,6 @@ private static boolean isUnsupportedConnectMethod(JsonRpcException ex) {
308312
*/
309313
public CompletableFuture<Void> stop() {
310314
var closeFutures = new ArrayList<CompletableFuture<Void>>();
311-
Executor exec = options.getExecutor();
312315

313316
for (CopilotSession session : new ArrayList<>(sessions.values())) {
314317
Runnable closeTask = () -> {
@@ -320,9 +323,7 @@ public CompletableFuture<Void> stop() {
320323
};
321324
CompletableFuture<Void> future;
322325
try {
323-
future = exec != null
324-
? CompletableFuture.runAsync(closeTask, exec)
325-
: CompletableFuture.runAsync(closeTask);
326+
future = CompletableFuture.runAsync(closeTask, executor);
326327
} catch (RejectedExecutionException e) {
327328
LOG.log(Level.WARNING, "Executor rejected session close task; closing inline", e);
328329
closeTask.run();
@@ -344,7 +345,7 @@ public CompletableFuture<Void> stop() {
344345
public CompletableFuture<Void> forceStop() {
345346
disposed = true;
346347
sessions.clear();
347-
return cleanupConnection();
348+
return cleanupConnection().whenComplete((ignored, error) -> shutdownOwnedExecutor());
348349
}
349350

350351
private CompletableFuture<Void> cleanupConnection() {
@@ -436,9 +437,7 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
436437

437438
long setupNanos = System.nanoTime();
438439
var session = new CopilotSession(sessionId, connection.rpc);
439-
if (options.getExecutor() != null) {
440-
session.setExecutor(options.getExecutor());
441-
}
440+
session.setExecutor(executor);
442441
SessionRequestBuilder.configureSession(session, config);
443442
sessions.put(sessionId, session);
444443
LoggingHelpers.logTiming(LOG, Level.FINE,
@@ -524,9 +523,7 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
524523
// Register the session before the RPC call to avoid missing early events.
525524
long setupNanos = System.nanoTime();
526525
var session = new CopilotSession(sessionId, connection.rpc);
527-
if (options.getExecutor() != null) {
528-
session.setExecutor(options.getExecutor());
529-
}
526+
session.setExecutor(executor);
530527
SessionRequestBuilder.configureSession(session, config);
531528
sessions.put(sessionId, session);
532529
LoggingHelpers.logTiming(LOG, Level.FINE,
@@ -923,6 +920,14 @@ public void close() {
923920
stop().get(AUTOCLOSEABLE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
924921
} catch (Exception e) {
925922
LOG.log(Level.FINE, "Error during close", e);
923+
} finally {
924+
shutdownOwnedExecutor();
925+
}
926+
}
927+
928+
private void shutdownOwnedExecutor() {
929+
if (ownedExecutor != null) {
930+
ownedExecutor.shutdown();
926931
}
927932
}
928933

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot;
6+
7+
import java.util.concurrent.Executor;
8+
import java.util.concurrent.ForkJoinPool;
9+
10+
final class DefaultExecutorProvider {
11+
12+
private DefaultExecutorProvider() {
13+
}
14+
15+
static Executor create() {
16+
return ForkJoinPool.commonPool();
17+
}
18+
19+
static boolean isOwned(Executor executor) {
20+
return false;
21+
}
22+
}

java/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,11 @@ public CopilotClientOptions setEnvironment(Map<String, String> environment) {
287287

288288
/**
289289
* Gets the executor used for internal asynchronous operations.
290+
* <p>
291+
* Returns {@code null} if no executor has been explicitly set, indicating that
292+
* the SDK should use its default executor strategy.
290293
*
291-
* @return the executor, or {@code null} to use the default
292-
* {@code ForkJoinPool.commonPool()}
294+
* @return the executor, or {@code null} if using SDK defaults
293295
*/
294296
public Executor getExecutor() {
295297
return executor;
@@ -299,15 +301,18 @@ public Executor getExecutor() {
299301
* Sets the executor used for internal asynchronous operations.
300302
* <p>
301303
* When provided, the SDK uses this executor for all internal
302-
* {@code CompletableFuture} combinators instead of the default
303-
* {@code ForkJoinPool.commonPool()}. This allows callers to isolate SDK work
304-
* onto a dedicated thread pool or integrate with container-managed threading.
304+
* {@code CompletableFuture} combinators. This allows callers to isolate SDK
305+
* work onto a dedicated thread pool or integrate with container-managed
306+
* threading.
305307
* <p>
306-
* Passing {@code null} reverts to the default {@code ForkJoinPool.commonPool()}
307-
* behavior.
308+
* The SDK will not shut down a user-provided executor. If you pass a custom
309+
* {@code ExecutorService}, you remain responsible for shutting it down.
310+
* <p>
311+
* If not set (or set to {@code null}), the SDK uses its default executor:
312+
* virtual threads on JDK 25+, {@code ForkJoinPool.commonPool()} on older JDKs.
308313
*
309314
* @param executor
310-
* the executor to use, or {@code null} for the default
315+
* the executor to use, or {@code null} for SDK defaults
311316
* @return this options instance for fluent chaining
312317
*/
313318
public CopilotClientOptions setExecutor(Executor executor) {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot;
6+
7+
import java.util.concurrent.Executor;
8+
import java.util.concurrent.ExecutorService;
9+
import java.util.concurrent.Executors;
10+
11+
final class DefaultExecutorProvider {
12+
13+
private DefaultExecutorProvider() {
14+
}
15+
16+
static Executor create() {
17+
return Executors.newVirtualThreadPerTaskExecutor();
18+
}
19+
20+
static boolean isOwned(Executor executor) {
21+
return executor instanceof ExecutorService;
22+
}
23+
}

0 commit comments

Comments
 (0)