ApacheHttp2Transport: send() hangs forever when an Error kills the I/O dispatcher thread (Error is never surfaced)
Maintainer thường phản hồi trong vòng 4 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 45/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- java
- Lĩnh vực
- backend, networking
Hướng nghiên cứu
Start with ApacheHttp2Request.execute() and the wait on responseFuture.get(), then read how ApacheHttp2Transport builds the async client and how its dispatcher handles uncaught errors. Reproduce with the supplied Repro.java steps, including the injected error and a nonzero read timeout. Done means a dispatcher Error no longer leaves send() blocked indefinitely and its cause is surfaced to the caller.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
[REQUIRED] Step 2: Describe your environment
- Operating System version: Linux (Ubuntu 24.04), OpenJDK 21
- Firebase SDK version: reproduced on 9.11.0 (latest) and 9.5.0
- Library version: httpclient5 5.6.4 / httpcore5 5.4.3 (9.11.0 defaults); 9.5.0 with httpcore5 + httpcore5-h2 5.3.6
- Firebase Product: messaging (affects every API using the default
ApacheHttp2Transport)
[REQUIRED] Step 3: Describe the problem
Since 9.4.0 (#979), the default transport is ApacheHttp2Transport, which runs network I/O on httpclient5's httpclient-dispatch-N threads. If an Error (not an Exception) is thrown on one of those threads, for example a LinkageError from a mismatched transitive dependency, an OutOfMemoryError, or a StackOverflowError:
- httpcore5's
IOReactorWorkerrethrows it and the dispatcher thread dies. The error is only printed to stderr by the JVM's default uncaught-exception handler. The SDK never observes it. TheFutureCallbackpassed inApacheHttp2Request.execute()is empty, and it isn't invoked in this case anyway. - The in-flight request's future is never completed or failed.
ApacheHttp2Request.execute()callsresponseFuture.get()with no bound whenreadTimeout == 0. That is theFirebaseOptionsdefault:TimeoutInitializeralways applies the options' timeouts, which overrides google-http-client's 20s defaults. The connect and response timeouts passed to httpclient5 are 0 (disabled), and they would be enforced by the dead dispatcher thread anyway.
Result: FirebaseMessaging.send() blocks the calling thread forever, with no exception and no SDK log.
With a non-zero read timeout (9.11.0 only, via #1239), the call fails after the timeout with Unknown error while making a remote service call: Stream exception in request, caused by TimeoutValueException. The real error isn't in the cause chain, so there is still no way to tell from the SDK what went wrong. Before the HTTP/2 transport, the same Error would have been thrown synchronously on the caller's thread.
Real-world impact. We hit this in production on 9.5.0. Our build resolved httpcore5/httpcore5-h2 5.3.6 next to httpclient5 (firebase-admin only declares httpclient5). httpcore5-h2 5.3.4 changed the ClientH2UpgradeHandler/ClientH2PrefaceHandler constructors that httpclient5 5.3.x/5.4.x call (apache/httpcomponents-core@048bcc1). Each new connection threw NoSuchMethodError on the dispatcher thread. The version skew was on our side, and Apache marks those classes @Internal. But every thread calling send() hung indefinitely, and push delivery silently stopped for about 48 hours with no exception, metric, or SDK log to point at the cause.
Steps to reproduce:
Repro.java uses only the public API plus the SDK's own ApacheHttp2Transport.defaultHttpAsyncClientBuilder(). It sends with a fake token, so a healthy client fails fast with a 401.
cs fetch --classpath com.google.firebase:firebase-admin:9.11.0 > cp.txt
java -cp "$(cat cp.txt)" Repro.java # control: 401 in ~330ms
java -cp "$(cat cp.txt)" Repro.java inject-error # hangs (still blocked after 60s)
java -cp "$(cat cp.txt)" Repro.java inject-error 5000 # "Stream exception in request" after 5s, real Error not in cause chain
# Natural trigger on 9.5.0 (no injection): hangs exactly the same way
cs fetch --classpath com.google.firebase:firebase-admin:9.5.0 \
--force-version org.apache.httpcomponents.core5:httpcore5:5.3.6 \
--force-version org.apache.httpcomponents.core5:httpcore5-h2:5.3.6 > cp-950.txt
java -cp "$(cat cp-950.txt)" Repro.java
Output with inject-error on 9.11.0:
Exception in thread "httpclient-dispatch-1" java.lang.LinkageError: simulated Error on the I/O dispatch thread
at Repro$ThrowingListener.connected(Repro.java:70)
at org.apache.hc.core5.reactor.InternalDataChannel.onTLSSessionStart(InternalDataChannel.java:184)
...
at org.apache.hc.core5.reactor.IOReactorWorker.run(IOReactorWorker.java:44)
RESULT: send() still blocked after 60s. fcm-caller stack:
at org.apache.hc.core5.concurrent.BasicFuture.get(BasicFuture.java:97)
at com.google.firebase.internal.ApacheHttp2Request.execute(ApacheHttp2Request.java:134)
at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:1012)
at com.google.firebase.internal.ErrorHandlingHttpClient.send(ErrorHandlingHttpClient.java:97)
...
at com.google.firebase.messaging.FirebaseMessaging.send(FirebaseMessaging.java:91)
On 9.5.0 with httpcore5 5.3.6, the dispatcher dies with java.lang.NoSuchMethodError: 'void org.apache.hc.core5.http2.impl.nio.ClientH2UpgradeHandler.<init>(org.apache.hc.core5.http2.impl.nio.ClientH2StreamMultiplexerFactory)' at HttpAsyncClientProtocolNegotiationStarter.createHandler. send() blocks at ApacheHttp2Request.java:123.
Relevant Code:
public class Repro {
public static void main(String[] args) throws Exception {
FirebaseOptions.Builder options = FirebaseOptions.builder()
.setProjectId("repro-project")
.setCredentials(new FakeCredentials());
if (args.length > 1) {
options.setReadTimeout(Integer.parseInt(args[1]));
}
if (args.length > 0 && args[0].equals("inject-error")) {
options.setHttpTransport(new ApacheHttp2Transport(
ApacheHttp2Transport.defaultHttpAsyncClientBuilder()
.setIOSessionListener(new ThrowingListener())
.build()));
}
FirebaseApp app = FirebaseApp.initializeApp(options.build());
Future<String> result = Executors.newSingleThreadExecutor().submit(() ->
FirebaseMessaging.getInstance(app).send(Message.builder().setToken("fake-device-token").build()));
// Healthy client: FirebaseMessagingException (401) in well under a second.
// Broken dispatcher with default options: never returns.
System.out.println(result.get(60, TimeUnit.SECONDS));
}
static final class FakeCredentials extends GoogleCredentials {
@Override
public AccessToken refreshAccessToken() {
return new AccessToken("fake-token", new Date(System.currentTimeMillis() + 3_600_000));
}
}
static final class ThrowingListener implements IOSessionListener {
@Override public void connected(IOSession s) { throw new LinkageError("simulated Error on the I/O dispatch thread"); }
@Override public void startTls(IOSession s) {}
@Override public void inputReady(IOSession s) {}
@Override public void outputReady(IOSession s) {}
@Override public void timeout(IOSession s) {}
@Override public void exception(IOSession s, Exception ex) {}
@Override public void disconnected(IOSession s) {}
}
}
Suggested fixes
- Never wait without a bound in
ApacheHttp2Request.execute(). Either ship finite default timeouts inFirebaseOptions, or bound the client-side wait even whenreadTimeout == 0. Today the SDK's liveness depends on a background thread it doesn't control. - Surface dispatcher death to callers. For example, build the default client with
HttpAsyncClientBuilder.setThreadFactory(...)so dispatcher threads have anUncaughtExceptionHandler. That handler can record theThrowableand fail outstanding and future requests with anIOExceptionwhose cause is theError. Alternatively, checkhttpAsyncClient.getStatus()while waiting. httpcore5's I/O reactor exception callback only receivesExceptions, so it doesn't cover this case. - Log it. The SDK already depends on SLF4J; logging dispatcher death there would make the root cause visible in applications' normal logs.
- Pin
httpcore5andhttpcore5-h2explicitly in the POM, at the version matching the declaredhttpclient5. Dependency resolution in consuming builds is then less likely to pair httpclient5 with an incompatible core. Apache doesn't guarantee compatibility for the@Internalclasses httpclient5 uses.
Workarounds for other users
- Set
FirebaseOptions.Builder#setConnectTimeoutandsetReadTimeout. On 9.11.0 and later, this turns the hang into a timeout error, though the root cause is still hidden. - Keep
httpcore5/httpcore5-h2aligned with thehttpclient5version the SDK declares.
- Ngôn ngữ chính
- Java
- Star
- 621
- Fork
- 306
- Merge trung bình
- 3 ngày 12 giờ
- Pull request đã merge (30 ngày)
- 12
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- 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 firebase/firebase-admin-java
-
google-http-client-jackson2 cannot be excluded even when a JsonFactory is configured via FirebaseOptionsCó thể đã có người làm @lahirumaramba đã nhận 14 ngày trước. Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
firebase/firebase-admin-java#1231 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 4 ngày
-
[BUG] RemoteConfig unit tests fail in non-English (e.g., Korean) Locales due to Date ParsingCó thể đã có người làm @rathovarun1032 đã nhận 213 ngày trước. Đang mởapi: remoteconfig
firebase/firebase-admin-java#1196 · 1 bình luận · 2 người được giao ·
Maintainer thường phản hồi trong vòng 4 ngày
-
Auth list tenants is not pageableCó thể đã có người làm @siddharthmohan619-eng đã nhận 218 ngày trước. Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 45/100
firebase/firebase-admin-java#1192 · 3 bình luận ·
Maintainer thường phản hồi trong vòng 4 ngày
-
needs-triage type: feature request
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 45/100
firebase/firebase-admin-java#1149 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 4 ngày
-
[FR] Expose `link` / `link_android` in notifications for deep linkingCó thể làm lại được @won-seoop đã nhận 145 ngày trước và không có pull request nào đang mở. Đang mởneeds-triage type: feature request
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 45/100
firebase/firebase-admin-java#1142 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 4 ngày
Tất cả issue của firebase/firebase-admin-java
Issue tương tự
-
`timer` rule rejects dates the engine acceptsCó thể đã có người làm @MFA-G đã nhận hôm nay. Đang mởbug ready
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
camunda/bpmnlint-plugin-camunda-compat#281 · 3 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
status: waiting-for-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
Maintainer thường phản hồi trong vòng 1 ngày
-
AOT compilation fails for derived delete queries returning SetCó thể đã có người làm @ryujungkyun đã nhận hôm nay. Đang mởstatus: waiting-for-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
spring-projects/spring-data-jpa#4387 ·
Maintainer thường phản hồi trong vòng 3 ngày