Async internal plumbing bypasses both `dispatcherExecutorService` and `streamHandlerExecutor`, forcing a hop through `ForkJoinPool.commonPool()`
メンテナーはふだん 1 日以内に返信
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 35/100
調査の方向性
ChatCompletionServiceAsyncImpl.kt と BatchServiceAsyncImpl.kt から始め、生成ファイルのヘッダーと CONTRIBUTING.md に記載されているジェネレーターの経路を確認してください。executor を指定しない thenComposeAsync/thenApplyAsync の呼び出しを追跡し、非同期サービス全体で必要となるテンプレートの変更を特定してください。再生成された実装が一貫して明示的に設定された executor を使用し、セキュリティコンテキストの再現が 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):
ChatCompletionServiceAsyncImpl.kt:161-186builds the request via.prepareAsync(...), which
returns an already-completed future (PrepareRequest.kt:27-33).ChatCompletionServiceAsyncImpl.kt:185:
No executor argument on an already-complete future ->.thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) }fnruns onForkJoinPool.commonPool(),
not on the caller's thread and not ondispatcherExecutorService.- From that commonPool thread,
OkHttpClient.kt:52-79'sexecuteAsynccallscall.enqueue(callback).
OkHttp'sDispatcher(configured viadispatcherExecutorService-OkHttpClient.kt:267,
dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) }) picks up the actual HTTP work from
here, so this hop is the onedispatcherExecutorServicegenuinely controls. future.complete(response.toResponse())(OkHttpClient.kt:62) runs insideCallback.onResponse,
on the dispatcher thread.- 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. - Finally,
AsyncStreamResponse.kt:94-133'swhenCompleteAsync({ ... }, executor)-executorhere
isclientOptions.streamHandlerExecutor(ChatCompletionServiceAsyncImpl.kt:77) or the
caller-suppliedsubscribe(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 callexecutor.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:
- Reuse
dispatcherExecutorServicefor 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. - Introduce a new, distinct
ClientOptionsexecutor (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.ManagedExecutorServicefor context/identity propagation.
- 主要言語
- Kotlin
- スター
- 1.5k
- フォーク
- 266
- 平均マージ
- 9時間 50分
- マージ済み PR(30日)
- 146
環境構築
このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
openai/openai-java のほかの issue
-
HttpRequest.url() logs spaces in path segments as literal plus signs対応中かも @sylvesterkaczmarek が 52 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
openai/openai-java#886 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
Make diarized transcription duration and task optional対応中かも @wskr00 が 83 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
openai/openai-java#802 · コメント 2 件 · リアクション 3 件 ·
メンテナーはふだん 1 日以内に返信
-
AsyncStreamResponse can hang when the subscriber executor rejects work対応中かも @sylvesterkaczmarek が 36 日前に担当しました。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 74/100
openai/openai-java#973 ·
メンテナーはふだん 1 日以内に返信
-
難易度 5/5 1週間以上 初心者へのやさしさ 35/100
openai/openai-java#956 ·
メンテナーはふだん 1 日以内に返信
-
DefaultSleeper.close can strand pending sleepAsync futures対応中かも @sylvesterkaczmarek が 52 日前に担当しました。 オープン
難易度 3/5 1〜2日 初心者へのやさしさ 72/100
openai/openai-java#889 ·
メンテナーはふだん 1 日以内に返信
openai/openai-java の issue をすべて見る
似ている issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
partiql/partiql-lang-kotlin#1972 ·
-
Cambio de urlオープンBug Domain changed
難易度 1/5 1時間未満 初心者へのやさしさ 72/100
keiyoushi/extensions-source#19783 ·
メンテナーはふだん 1 日以内に返信
-
afk-ok area:data-quality importer size:S
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
enorm-labs/event-junkie#3027 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
メンテナーはふだん 1 日以内に返信