Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Synchronous response parsing pins Java 21 virtual threads during body reads

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

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

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

評価

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

調査の方向性

Start with HttpResponseFor.kt, where parseable wraps the parsed response in Kotlin's synchronized lazy, and trace how the initializer reads the body through ObjectMapper.readValue. The pinning repro in the issue is a standalone Java program with a raw control mode, so run it before and after a change. Done means parsed mode no longer reports SynchronizedLazyImpl in jdk.VirtualThreadPinned events while one-time parsing semantics stay intact.

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

説明

Problem

Synchronous non-streaming calls can pin Java 21 virtual threads while waiting for response body data, including with the SDK's default OkHttp transport.

HttpResponseFor.kt caches the parsed response using Kotlin's default synchronized lazy. Its initializer performs JSON parsing and reads the response body while holding the lazy monitor. If that read blocks, the virtual thread cannot unmount from its carrier on Java 21.

With slow responses and concurrent calls, this can occupy the available carriers and delay unrelated virtual-thread work.

Reproduction conditions

  • Java 21.0.4
  • openai-java-client-okhttp:4.71.0 (default OkHttp 4.12.0)
  • JVM option: -Djdk.virtualThreadScheduler.parallelism=2
  • A local HTTP/1.1 server running on platform threads returns a valid non-streaming chat completion: send the response headers and the first byte of JSON, flush, wait 500ms, then send the remaining body.
  • Run client.chat().completions().create(params) on virtual threads, submitting seven calls together per round, for three rounds.
  • Record JFR jdk.VirtualThreadPinned events with a 10ms threshold and stack traces.

Observed pinning

JFR recorded nine pinning events containing SynchronizedLazyImpl, with a maximum duration of approximately 505ms. A representative stack, shown from the call entry to the blocking read, is:

ChatCompletionServiceImpl.create
HttpResponseForKt$parseable$1.parse
kotlin.SynchronizedLazyImpl.getValue
HttpResponseForKt$parseable$1$parsed$1.invoke
ObjectMapper.readValue
okhttp3.internal.connection.Exchange$ResponseBodySource.read
okhttp3.internal.http1.Http1ExchangeCodec$FixedLengthSource.read
okio.InputStreamSource.read
sun.nio.ch.NioSocketImpl.timedRead
sun.nio.ch.Poller.pollIndirect
java.lang.VirtualThread.parkOnCarrierThread

As a control, reading response.body() directly after withRawResponse().create(params), under the same slow-body conditions, produced no pinning events above the threshold.

Expected behavior

Response parsing should preserve its one-time initialization semantics without holding a JVM monitor across blocking response-body reads, so synchronous calls can release their carriers while waiting for I/O on Java 21.

Runnable reproduction

Use JDK 21 and Maven. The program uses only a local server and a dummy API key.

Save this as pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>repro</groupId>
  <artifactId>virtual-thread-pinning</artifactId>
  <version>1.0</version>
  <dependencies>
    <dependency>
      <groupId>com.openai</groupId>
      <artifactId>openai-java-client-okhttp</artifactId>
      <version>4.71.0</version>
    </dependency>
  </dependencies>
</project>

Save this as PinningRepro.java in the same directory:

import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.sun.net.httpserver.HttpServer;
import jdk.jfr.Recording;
import jdk.jfr.consumer.RecordingFile;

import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class PinningRepro {
    public static void main(String[] args) throws Exception {
        boolean raw = args.length > 0 && args[0].equals("raw");
        byte[] json = ("{\"id\":\"test\",\"object\":\"chat.completion\",\"created\":1,"
                + "\"model\":\"test\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\","
                + "\"content\":\"ok\"},\"finish_reason\":\"stop\"}]}")
                .getBytes(StandardCharsets.UTF_8);
        var serverThreads = Executors.newFixedThreadPool(16, Thread.ofPlatform().factory());
        var server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
        server.setExecutor(serverThreads);
        server.createContext("/v1/chat/completions", exchange -> {
            try (exchange) {
                exchange.getRequestBody().readAllBytes();
                exchange.getResponseHeaders().set("Content-Type", "application/json");
                exchange.sendResponseHeaders(200, json.length);
                var out = exchange.getResponseBody();
                out.write(json, 0, 1);
                out.flush();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException error) {
                    Thread.currentThread().interrupt();
                    return;
                }
                out.write(json, 1, json.length - 1);
            }
        });
        server.start();
        var client = OpenAIOkHttpClient.builder()
                .baseUrl("http://127.0.0.1:" + server.getAddress().getPort() + "/v1")
                .apiKey("dummy").maxRetries(0).timeout(Duration.ofSeconds(5)).build();
        var params = ChatCompletionCreateParams.builder().model("test").addUserMessage("test").build();
        Path file = Path.of(raw ? "raw.jfr" : "pinning.jfr");
        try (var recording = new Recording();
             var callers = Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("repro-", 1).factory())) {
            // Exclude class initialization and the platform-thread warmup from JFR.
            client.chat().completions().create(params);
            recording.enable("jdk.VirtualThreadPinned").withThreshold(Duration.ofMillis(10)).withStackTrace();
            recording.start();
            for (int round = 0; round < 3; round++) {
                var start = new CountDownLatch(1);
                var futures = new ArrayList<Future<?>>();
                for (int i = 0; i < 7; i++) {
                    futures.add(callers.submit(() -> {
                        start.await();
                        if (raw) {
                            try (var response = client.chat().completions().withRawResponse().create(params)) {
                                response.body().readAllBytes();
                            }
                        } else {
                            client.chat().completions().create(params);
                        }
                        return null;
                    }));
                }
                start.countDown();
                for (var future : futures) {
                    future.get(10, TimeUnit.SECONDS);
                }
            }
            recording.stop();
            recording.dump(file);
        } finally {
            client.close();
            server.stop(0);
            serverThreads.shutdownNow();
        }
        var pins = RecordingFile.readAllEvents(file).stream()
                .filter(e -> e.getEventType().getName().equals("jdk.VirtualThreadPinned")).toList();
        System.out.println("mode=" + (raw ? "raw" : "parsed") + ", completed=21, pinned events=" + pins.size());
        pins.stream().findFirst().ifPresent(System.out::println);
    }
}

Run:

mvn -q dependency:build-classpath -Dmdep.outputFile=classpath.txt
javac --release 21 -cp "$(cat classpath.txt)" PinningRepro.java
java -Djdk.virtualThreadScheduler.parallelism=2 -cp ".:$(cat classpath.txt)" PinningRepro
jfr print --events jdk.VirtualThreadPinned pinning.jfr

# Control: read the raw response body without SDK lazy parsing.
java -Djdk.virtualThreadScheduler.parallelism=2 -cp ".:$(cat classpath.txt)" PinningRepro raw

The parsed mode produces pinning events whose full JFR stacks include SynchronizedLazyImpl and the socket read. The raw mode is the control. Event counts vary with scheduling; the standalone program produced eight events in my Java 21.0.4 run.

主要言語
Kotlin
スター
1.5k
フォーク
266
平均マージ
10時間 11分
マージ済み PR(30日)
141

環境構築

Codespaces で開く

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

はじめの一歩

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

openai/openai-java のほかの issue

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

似ている issue

Kotlin の issue をもっと見る

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

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