Skip to content

Commit 60a3e48

Browse files
committed
perf: standardize non-blocking OkHttp API
1 parent c338c67 commit 60a3e48

7 files changed

Lines changed: 434 additions & 300 deletions

File tree

src/main/java/io/github/easy4j/opencode/OpenCodeClient.java

Lines changed: 25 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@
1717
import java.util.List;
1818
import java.util.Map;
1919
import java.util.Objects;
20-
import java.util.concurrent.ExecutorService;
21-
import java.util.concurrent.LinkedBlockingQueue;
22-
import java.util.concurrent.RejectedExecutionException;
23-
import java.util.concurrent.ThreadPoolExecutor;
24-
import java.util.concurrent.TimeUnit;
25-
import java.util.concurrent.atomic.AtomicInteger;
20+
import java.util.concurrent.CompletableFuture;
2621
import java.util.function.Consumer;
2722

2823
/**
@@ -65,7 +60,6 @@ public class OpenCodeClient implements AutoCloseable {
6560
private final OpenCodeChatClient chatClient;
6661
private final OpenCodeSseClient sseClient;
6762
private final OpenCodeCli cli;
68-
private final ExecutorService streamExecutor;
6963

7064
// ============================================================
7165
// 构造器
@@ -128,7 +122,6 @@ public OpenCodeClient(OpenCodeHttpClientConfig httpConfig, OpenCodeCliConfig cli
128122
this.chatClient = null;
129123
this.sseClient = null;
130124
}
131-
this.streamExecutor = createStreamExecutor(httpConfig);
132125

133126
// CLI 子系统初始化
134127
if (cliEnabled) {
@@ -167,21 +160,6 @@ public OpenCodeClient(OpenCodeClientConfig config,
167160
this.chatClient = httpClient instanceof OpenCodeChatClient ? (OpenCodeChatClient) httpClient : null;
168161
this.sseClient = sseClient;
169162
this.cli = cli;
170-
this.streamExecutor = createStreamExecutor(config.getHttp());
171-
}
172-
173-
private static ExecutorService createStreamExecutor(OpenCodeHttpClientConfig config) {
174-
int corePoolSize = Math.max(1, config.getStreamCorePoolSize());
175-
int maxPoolSize = Math.max(corePoolSize, config.getStreamMaxPoolSize());
176-
AtomicInteger threadIndex = new AtomicInteger();
177-
return new ThreadPoolExecutor(corePoolSize, maxPoolSize,
178-
Math.max(1L, config.getStreamKeepAliveMillis()), TimeUnit.MILLISECONDS,
179-
new LinkedBlockingQueue<>(Math.max(1, config.getStreamQueueCapacity())), runnable -> {
180-
Thread thread = new Thread(runnable,
181-
"opencode-stream-consumer-" + threadIndex.incrementAndGet());
182-
thread.setDaemon(true);
183-
return thread;
184-
}, new ThreadPoolExecutor.AbortPolicy());
185163
}
186164

187165
// ============================================================
@@ -341,7 +319,7 @@ public PromptResult chatCompletionWithSession(String text, String providerID, St
341319
return httpClient.chatCompletionWithSession(PromptRequest.ofText(text, providerID, modelID), sessionKey);
342320
}
343321

344-
public boolean chatCompletionWithSessionAsync(PromptRequest request, String sessionKey) {
322+
public CompletableFuture<Boolean> chatCompletionWithSessionAsync(PromptRequest request, String sessionKey) {
345323
return httpClient.chatCompletionWithSessionAsync(request, sessionKey);
346324
}
347325

@@ -355,6 +333,11 @@ public ChatResponse chatCompletion(String sessionId, ChatRequest request) {
355333
return ChatMessageMapper.toChatResponse(result);
356334
}
357335

336+
/** 异步完成指定会话的聊天请求。 */
337+
public CompletableFuture<ChatResponse> chatCompletionAsync(String sessionId, ChatRequest request) {
338+
return chatClient.chatCompletionAsync(sessionId, request);
339+
}
340+
358341
public ChatResponse chatCompletionWithSession(ChatRequest request, String sessionKey) {
359342
PromptRequest promptRequest = ChatMessageMapper.toPromptRequest(request);
360343
PromptResult result = httpClient.chatCompletionWithSession(promptRequest, sessionKey);
@@ -368,6 +351,13 @@ public ChatResponse chatCompletionWithSession(ChatRequest request, String sessio
368351
return ChatMessageMapper.toChatResponse(result);
369352
}
370353

354+
/** 异步查找会话并完成聊天请求。 */
355+
public CompletableFuture<ChatResponse> chatCompletionWithSessionAsync(ChatRequest request,
356+
String sessionKey,
357+
HttpCallCancellation cancellation) {
358+
return chatClient.chatCompletionWithSessionAsync(request, sessionKey, cancellation);
359+
}
360+
371361
public StreamingChatResponse chatCompletionStream(ChatRequest request, String sessionKey) {
372362
return chatCompletionStream(request, sessionKey, null);
373363
}
@@ -383,117 +373,23 @@ public StreamingChatResponse chatCompletionStream(ChatRequest request, String se
383373
public StreamingChatResponse chatCompletionStream(ChatRequest request, String sessionKey,
384374
OpenCodeRequestContext context,
385375
Consumer<String> deltaConsumer) {
386-
String sessionId = httpClient.ensureSession(sessionKey, context);
387-
PromptRequest promptRequest = ChatMessageMapper.toPromptRequest(request);
388-
389-
StreamingChatResponse stream = new StreamingChatResponse().onDelta(deltaConsumer);
390-
391-
OpenCodeSseClient.QueueSubscription subscription =
392-
sseClient.subscribeQueueSubscription(context);
393-
java.util.concurrent.BlockingQueue<Event> queue = subscription.getQueue();
394-
395-
try {
396-
streamExecutor.submit(() -> {
397-
try {
398-
long deadline = System.currentTimeMillis() + (config.getCli().getTimeout() * 1000L);
399-
while (!stream.isDone() && System.currentTimeMillis() < deadline) {
400-
Event event = queue.poll(3, java.util.concurrent.TimeUnit.SECONDS);
401-
if (event == null) {
402-
continue;
403-
}
404-
405-
String eventSessionId = event.getProperties() != null
406-
? Objects.toString(event.getProperties().get("sessionID"), null) : null;
407-
if (eventSessionId == null || !eventSessionId.equals(sessionId)) {
408-
continue;
409-
}
410-
411-
String type = event.getType();
412-
if (type == null) {
413-
continue;
414-
}
415-
416-
if (type.contains("text.delta") || type.contains("message.part.updated")) {
417-
String delta = extractDeltaText(event);
418-
if (delta != null && !delta.isEmpty()) {
419-
stream.acceptDelta(delta);
420-
}
421-
}
422-
423-
if (type.contains("session.status") || type.contains("session.idle")) {
424-
String status = event.getProperties() != null
425-
? Objects.toString(event.getProperties().get("status"), null) : null;
426-
if ("idle".equals(status) || type.contains("idle")) {
427-
stream.finish();
428-
return;
429-
}
430-
}
431-
432-
if (type.contains("session.error")) {
433-
String error = event.getProperties() != null
434-
? Objects.toString(event.getProperties().get("error"), "unknown error") : "unknown error";
435-
stream.fail(new RuntimeException(error));
436-
return;
437-
}
438-
}
439-
if (!stream.isDone()) {
440-
stream.fail(new RuntimeException("Stream timed out for session: " + sessionId));
441-
}
442-
} catch (InterruptedException e) {
443-
Thread.currentThread().interrupt();
444-
stream.fail(e);
445-
} catch (Exception e) {
446-
stream.fail(e);
447-
} finally {
448-
subscription.close();
449-
}
450-
});
451-
} catch (RejectedExecutionException error) {
452-
subscription.close();
453-
stream.fail(new IllegalStateException("OpenCode stream executor is full", error));
376+
if (Objects.isNull(chatClient)) {
377+
StreamingChatResponse stream = new StreamingChatResponse();
378+
stream.fail(new IllegalStateException("OpenCodeChatClient is not configured"));
454379
return stream;
455380
}
456-
457-
try {
458-
if (!httpClient.promptAsync(sessionId, promptRequest, context)) {
459-
subscription.close();
460-
stream.fail(new IllegalStateException("OpenCode async prompt was rejected"));
461-
}
462-
} catch (RuntimeException error) {
463-
subscription.close();
464-
stream.fail(error);
465-
}
466-
467-
return stream;
468-
}
469-
470-
private static String extractDeltaText(Event event) {
471-
if (event.getProperties() == null) {
472-
return null;
473-
}
474-
Object part = event.getProperties().get("part");
475-
if (part instanceof Map) {
476-
Object text = ((Map<?, ?>) part).get("text");
477-
if (text != null) {
478-
return text.toString();
479-
}
480-
}
481-
Object delta = event.getProperties().get("delta");
482-
if (delta != null) {
483-
return delta.toString();
484-
}
485-
return null;
381+
return chatClient.chatCompletionStream(request, sessionKey, context, deltaConsumer);
486382
}
487383

488384
public String ensureSession(String sessionKey) {
489385
return httpClient.ensureSession(sessionKey);
490386
}
491387

492-
public boolean chatCompletionAsync(String sessionId, PromptRequest request) {
388+
public CompletableFuture<Boolean> chatCompletionAsync(String sessionId, PromptRequest request) {
493389
return httpClient.promptAsync(sessionId, request);
494390
}
495391

496-
public boolean chatCompletionAsync(String sessionId, String text) {
392+
public CompletableFuture<Boolean> chatCompletionAsync(String sessionId, String text) {
497393
return httpClient.promptAsync(sessionId, PromptRequest.ofText(text));
498394
}
499395

@@ -521,6 +417,11 @@ public HealthStatus health() {
521417
return httpClient.health();
522418
}
523419

420+
/** 异步检查 OpenCode Server 健康状态。 */
421+
public CompletableFuture<HealthStatus> healthAsync() {
422+
return httpClient.healthAsync();
423+
}
424+
524425
// ============================================================
525426
// SSE 事件流
526427
// ============================================================
@@ -976,7 +877,6 @@ public io.github.easy4j.opencode.cli.OpenCodeCliResult cliPr(int number) {
976877

977878
@Override
978879
public void close() {
979-
if (streamExecutor != null) streamExecutor.shutdownNow();
980880
if (httpClient != null) httpClient.close();
981881
if (sseClient != null) sseClient.close();
982882
}

src/main/java/io/github/easy4j/opencode/OpenCodeOkHttpClientFactory.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
import okhttp3.OkHttpClient;
66

77
import java.util.Objects;
8+
import java.util.concurrent.LinkedBlockingQueue;
9+
import java.util.concurrent.ThreadPoolExecutor;
810
import java.util.concurrent.TimeUnit;
11+
import java.util.concurrent.atomic.AtomicInteger;
912

1013
/**
1114
* Factory for creating {@link OkHttpClient} instances used by the OpenCode SDK in standalone mode.
@@ -30,8 +33,18 @@ private OpenCodeOkHttpClientFactory() {
3033
*/
3134
public static OkHttpClient create(OpenCodeHttpClientConfig config) {
3235
Objects.requireNonNull(config, "config");
33-
Dispatcher dispatcher = new Dispatcher();
34-
dispatcher.setMaxRequests(Math.max(1, config.getMaxRequests()));
36+
int maxRequests = Math.max(1, config.getMaxRequests());
37+
AtomicInteger threadIndex = new AtomicInteger();
38+
ThreadPoolExecutor executor = new ThreadPoolExecutor(maxRequests, maxRequests, 60L, TimeUnit.SECONDS,
39+
new LinkedBlockingQueue<>(maxRequests), runnable -> {
40+
Thread thread = new Thread(runnable,
41+
"opencode-okhttp-dispatcher-" + threadIndex.incrementAndGet());
42+
thread.setDaemon(true);
43+
return thread;
44+
});
45+
executor.allowCoreThreadTimeOut(true);
46+
Dispatcher dispatcher = new Dispatcher(executor);
47+
dispatcher.setMaxRequests(maxRequests);
3548
dispatcher.setMaxRequestsPerHost(Math.max(1, config.getMaxRequestsPerHost()));
3649
ConnectionPool connectionPool = new ConnectionPool(
3750
Math.max(1, config.getMaxIdleConnections()),

0 commit comments

Comments
 (0)