`PhantomReachableExecutorService` is never cleaned up because its Cleaner action captures the wrapper

オープン 初心者向け
#1,337 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
78/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
java, kotlin
領域
backend

調査の方向性

まず PhantomReachableExecutorService と、issue に示されている closeWhenPhantomReachable の登録を確認します。アクションが delegate をどのように捕捉するかを調べてから、executor のクリーンアップと弱参照に関する既存のテストを探します。wrapper が到達不能になった後に回収され、executor がシャットダウンされれば完了です。提案された修正はコンパイルも実行もされていません。

索引モデルが issue の本文から書いたものです。

説明

Problem

PhantomReachableExecutorService is meant to shut down its executor once the wrapper is only phantom reachable.
It can't, because the action registered with the Cleaner holds a strong reference to the wrapper:

internal class PhantomReachableExecutorService(private val executorService: ExecutorService) :
    ExecutorService {
    init {
        closeWhenPhantomReachable(this) { executorService.shutdown() }
    }

Here executorService compiles to a read of the private property through this.
The generated lambda has a this$0 field, as javap -p -c 'com.openai.core.PhantomReachableExecutorService$1' shows on 4.49.0, 4.80.0 and 4.99.0:

final com.openai.core.PhantomReachableExecutorService this$0;
...
invokestatic  com/openai/core/PhantomReachableExecutorService.access$getExecutorService$p
invokeinterface java/util/concurrent/ExecutorService.shutdown:()V

The Cleaner holds the action strongly, so the wrapper and its executor stay reachable for the life of the JVM.

Impact

ClientOptions.Builder.build() creates this wrapper by default, and streamHandlerExecutor(...) applies it to any ExecutorService.
So every client strands one wrapper and one ThreadPoolExecutor, whether or not it ever streams.
Calling close() shuts the pool down but doesn't release it.
This matters to applications that build clients repeatedly, for example one per tenant API key.

Reproduction

Build 2,000 sync and async client pairs, discard them, then take a class histogram with jcmd <pid> GC.class_histogram, which forces a full GC.

Client setup Retained PhantomReachableExecutorService
Default 4,000
Default, calling close() on each client 4,000
streamHandlerExecutor given a plain Executor 0

Clients, sleepers and HTTP client wrappers were released in all three.

Suggested fix

Make the action capture the delegate rather than the wrapper, for example by registering it from a function that doesn't have this in scope, or by copying the delegate into a local first:

init {
    val delegate = executorService
    closeWhenPhantomReachable(this) { delegate.shutdown() }
}

This suggestion hasn't been compiled or run.
A regression test could register a wrapper, drop it, and assert through a WeakReference that it's collected after System.gc(), or that the delegate's isShutdown becomes true.

Workaround

Pass an Executor that isn't an ExecutorService to ClientOptions.Builder.streamHandlerExecutor(...).

主要言語
Kotlin
スター
1.6k
フォーク
275
平均マージ
10時間 37分
マージ済み PR(30日)
171

環境構築

Codespaces で開く

このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

openai/openai-java のほかの issue

openai/openai-java の issue をすべて見る

似ている issue

Kotlin の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。