Synchronous response parsing pins Java 21 virtual threads during body reads
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 3/5
- Thời gian dự kiến
- Nửa ngày
- Mức phù hợp với người mới
- 42/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Lĩnh vực
- api, performance
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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.VirtualThreadPinnedevents 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.
- Ngôn ngữ chính
- Kotlin
- Star
- 1.5k
- Fork
- 266
- Merge trung bình
- 11 giờ 31 phút
- Pull request đã merge (30 ngày)
- 150
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của openai/openai-java
-
HttpRequest.url() logs spaces in path segments as literal plus signsCó thể đã có người làm @sylvesterkaczmarek đã nhận 56 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
openai/openai-java#886 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Make diarized transcription duration and task optionalCó thể đã có người làm @wskr00 đã nhận 86 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
openai/openai-java#802 · 2 bình luận · 3 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
AsyncStreamResponse can hang when the subscriber executor rejects workCó thể đã có người làm @sylvesterkaczmarek đã nhận 39 ngày trước. Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 74/100
openai/openai-java#973 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
openai/openai-java#956 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Async internal plumbing bypasses both `dispatcherExecutorService` and `streamHandlerExecutor`, forcing a hop through `ForkJoinPool.commonPool()`Có thể đã có người làm @sylvesterkaczmarek đã nhận 33 ngày trước. Đang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
openai/openai-java#952 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của openai/openai-java
Issue tương tự
-
[BUG] Navigation drawer drops library-name suffixes without ellipsis (1.0.8, NVIDIA Shield)Có thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
damontecres/Wholphin#2069 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Android (vision-camera-location): HybridLocationManager never quits its HandlerThread on disposeĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
margelo/react-native-vision-camera#4217 ·
Maintainer thường phản hồi trong vòng 4 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
saisaharsh3/Leftovers#13 · 1 bình luận ·
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 67/100
navikt/esyfo-narmesteleder#700 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
nightscout/AndroidAPS#5280 ·
Maintainer thường phản hồi trong vòng 1 ngày