Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

Async internal plumbing bypasses both `dispatcherExecutorService` and `streamHandlerExecutor`, forcing a hop through `ForkJoinPool.commonPool()`

未关闭
#952 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

@sylvesterkaczmarek 已经在做这个了。

开始于 2026年9月10日。

  • #990 来自 @sylvesterkaczmarek —— 未关闭

评估

难度
5/5
预计耗时
一周以上
新手友好度
35/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
活跃
技术栈
java, kotlin
领域
api, backend

调研方向

从 ChatCompletionServiceAsyncImpl.kt 和 BatchServiceAsyncImpl.kt 开始,然后检查生成文件头和 CONTRIBUTING.md 所描述的生成器路径。跟踪不带 executor 的 thenComposeAsync/thenApplyAsync 调用,并确定所有异步服务所需的模板变更。完成的标准是:重新生成的实现统一使用显式配置的 executor,并且 security-context reproduction 不再到达 ForkJoinPool.commonPool()。

由索引模型根据 Issue 内容生成。

描述

Sorry this issue is AI generated, I ran into this problem while trying to fix my issues with the security context. I think this would allow me to solve it a bit cleaner than i currently can. If you dont allow or want AI generated issues feel free to close this.

Summary

Every generated async service implementation (48 files, e.g. ChatCompletionServiceAsyncImpl.kt,
EmbeddingServiceAsyncImpl.kt, ResponseServiceAsyncImpl.kt, ...) chains its internal
CompletableFuture composition with the no-executor overloads, e.g.:

// ChatCompletionServiceAsyncImpl.kt:185
.thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) }

Per plain CompletableFuture semantics, thenComposeAsync(fn) with no executor argument runs fn on
ForkJoinPool.commonPool() - a JVM-wide, shared pool that has no relationship to either
ClientOptions.Builder.dispatcherExecutorService(...) or .streamHandlerExecutor(...). Both of those
builder methods are documented as ways for a caller to control the SDK's threading, but this internal
hop is invisible to both of them and cannot be configured through any public API.

Why this matters

In a Jakarta EE / Java EE environment, thread identity/security-context propagation for a
ManagedExecutorService is captured from whichever thread calls .execute()/.submit(). If an
application supplies a container-managed executor via dispatcherExecutorService/
streamHandlerExecutor specifically to get correct context propagation into async callbacks, this
ForkJoinPool.commonPool() hop is a gap neither setting can close: the pool's own worker threads are
plain JDK threads, created once and reused for the lifetime of the JVM, with no Jakarta EE context
association at all. This can result in security-context confusion (the wrong "current user" being
resolved) for any application relying on ambient/thread-local identity propagation while integrating
this SDK's async APIs - the exact failure mode we hit and traced in detail (see reproduction below).

Reproduction / trace (traced against 4.31.0)

Full call chain for client.async().chat().completions().createStreaming(params).subscribe(handler, executor):

  1. ChatCompletionServiceAsyncImpl.kt:161-186 builds the request via .prepareAsync(...), which
    returns an already-completed future (PrepareRequest.kt:27-33).
  2. ChatCompletionServiceAsyncImpl.kt:185:
    .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) }
    
    No executor argument on an already-complete future -> fn runs on ForkJoinPool.commonPool(),
    not on the caller's thread and not on dispatcherExecutorService.
  3. From that commonPool thread, OkHttpClient.kt:52-79's executeAsync calls call.enqueue(callback).
    OkHttp's Dispatcher (configured via dispatcherExecutorService - OkHttpClient.kt:267,
    dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) }) picks up the actual HTTP work from
    here, so this hop is the one dispatcherExecutorService genuinely controls.
  4. future.complete(response.toResponse()) (OkHttpClient.kt:62) runs inside Callback.onResponse,
    on the dispatcher thread.
  5. That triggers, synchronously on the same dispatcher thread: retry bookkeeping
    (RetryingHttpClient.kt:101-129, deliberately same-thread - "Run in the same thread."), then
    ChatCompletionServiceAsyncImpl.kt:186/76's .thenApply { ... } chain.
  6. Finally, AsyncStreamResponse.kt:94-133's whenCompleteAsync({ ... }, executor) - executor here
    is clientOptions.streamHandlerExecutor (ChatCompletionServiceAsyncImpl.kt:77) or the
    caller-supplied subscribe(handler, executor) argument. This is the only hop of the whole chain
    that either public builder setting actually reaches - and it's reached only because the dispatcher
    thread (step 4) happens to call executor.execute(...) at this point.

So: dispatcherExecutorService controls step 3 only; streamHandlerExecutor/subscribe's executor
argument controls step 6 only. Step 2 - the very first async hop, before either configured executor is
ever touched - always runs on ForkJoinPool.commonPool(), unconditionally, for every async service
call in the SDK.

(Confirmed directly by reading openai-java-core-4.31.0-sources.jar and
openai-java-client-okhttp-4.31.0-sources.jar; standard CompletableFuture.thenComposeAsync(fn)
executor-selection semantics are per the JDK spec, not inferred.)

Scope

The same .thenComposeAsync { ... } / .thenApplyAsync { ... } no-executor pattern appears in 48
files under com.openai.services.async.** (grep across the 4.31.0 core sources jar), e.g.
BatchServiceAsyncImpl.kt, EmbeddingServiceAsyncImpl.kt, ResponseServiceAsyncImpl.kt,
FileServiceAsyncImpl.kt, VectorStoreServiceAsyncImpl.kt, and 43 others - this is a
code-generation-template-level issue, not isolated to chat completions.

Reconfirmed on 4.54.0 (the current release at time of filing): BatchServiceAsyncImpl.kt's
create/retrieve/list/cancel all still contain the identical
request.thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } with no
executor argument, so this is not something already fixed between 4.31.0 and the latest release.

Suggested direction

Thread an explicit executor through the first .thenComposeAsync/.thenApplyAsync call in each
generated service implementation, rather than relying on the no-arg overload's default
(ForkJoinPool.commonPool()). Possible approaches, in rough order of how much they change the public
API:

  1. Reuse dispatcherExecutorService for this hop too, since it's already positioned as "the executor
    for running HTTP requests" and this hop exists purely to kick off that HTTP request.
  2. Introduce a new, distinct ClientOptions executor (e.g. internalAsyncExecutor) if the maintainers
    consider "the executor that runs HTTP requests" and "the executor that does internal
    CompletableFuture bookkeeping before the HTTP call" to be conceptually different things.

Either way, since this is generated code (each file's header states "File generated from our OpenAPI
spec by Castiron. See CONTRIBUTING.md for details."; CONTRIBUTING.md itself says "Most of the SDK is
generated code. Modifications to code will be persisted between generations, but may result in merge
conflicts..."), a complete fix needs to happen in the generator template, not just in the 48 checked-in
files, for it to apply consistently and survive regeneration across the whole SDK.

Environment
  • com.openai:openai-java-core, com.openai:openai-java-client-okhttp - reproduced on both 4.31.0 and
    the current 4.54.0.
  • Found while integrating streaming chat completions into a Jakarta EE 8 (Payara 5) application using
    javax.enterprise.concurrent.ManagedExecutorService for context/identity propagation.
主要语言
Kotlin
星标
1.5k
派生
268
平均合并
11 小时 31 分钟
30 天内合并 PR
150

环境准备

在 Codespaces 中打开

在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

openai/openai-java 的其他 Issue

查看 openai/openai-java 的全部 Issue

相似的 Issue

更多 Kotlin Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。