Repository navigation
fix(h2): clamp driver poll to armed stall deadline for indefinite timeouts - #52
detail-app[bot] wants to merge 2 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #52 +/- ##
=========================================
Coverage 86.78% 86.79%
+ Complexity 616 615 -1
=========================================
Files 60 60
Lines 16912 16981 +69
Branches 490 490
=========================================
+ Hits 14677 14738 +61
- Misses 1911 1920 +9
+ Partials 324 323 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Merging this PR will not alter performance
|
The new next_deadline test opens a real driver via elide_transport_driver_new, which builds an io_uring proactor unsupported under miri. Add the same cfg_attr(miri, ignore) gate that every other driver-opening test in this module already carries, per crates/bemo/AGENTS.md.
Detail bug report: View on Detail
Change
The
FLOW_CONTROL_STALL(5 s) safety net that force-resets a freed HTTP/2 stream stuck on withheld flow-control credit was implemented as a lazyInstant::now()comparison insideh2_socket::drive(). The stall detector is only evaluated whendrive()runs, which happens afterelide_transport_driver_pollreturns frompoll_batch. When an embedder polls with the documented indefinite timeout (timeout_ns = u64::MAX), the driver blocks inproactor.poll(None)with no internal timer or self-wake, sodrive()never re-runs and the 5 s deadline is never evaluated — the freed stream strands past its deadline for as long as the host blocks.Affected layers: Rust only (
crates/bemo); no ABI symbol, layout, or ownership changes. The fix is internal to the driver's poll loop and h2 socket state.Fix: Two-part change that honors armed deadlines inside the driver's own wait:
H2Socket::stall_deadline()exposes the earliest armedfreed_at + FLOW_CONTROL_STALLacross all streams on a socket;http::next_deadline(state)composes the minimum across all h2 sockets.elide_transport_driver_pollandpoll_callbackclamp the blocking poll timeout to the remaining time until that deadline, soDuration::MAXbecomes "sleep until the earliest internal deadline." Afterpoll_batchreturns,http::drive_armed_deadlines(state, events)re-drives every socket with an armed stall detector so the lazyInstant::now()verdict indrive()fires even when no I/O woke the driver. The stalled socket is not on the retry queue, soretry_deferredalone cannot reach it.Closes #47
Validation
Platform: Linux x86_64 (io_uring and epoll backends).
cargo build --workspace --locked— passescargo fmt --all --check— cleancargo clippy --workspace --all-targets --locked -- -D warnings— no warningscargo check --workspace --no-default-features --locked— passes (external allocator path)cargo test --workspace --doc --locked— passescargo nextest run -p bemo --lib --tests --locked --no-fail-fast— 379/381 pass; 2 pre-existing failures (persistent_keepalive_reuses_receive_registration_and_fixed_send_state,arm_receive_failure_leaves_socket_dormant) confirmed on the unmodified baselinecargo nextest run -p bemo --test http --locked --no-fail-fast— 70/71 pass; same 1 pre-existing failureELIDE_TRANSPORT_TEST_BACKEND=epoll cargo nextest run -p bemo --test http --locked— both stall detector tests pass on the epoll/polling backendcargo nextest run -p bemo-ffi --locked— passesmake test-miri— passes (installedmiricomponent; library unit tests including the newnext_deadlinetest run under Miri)make test-asan/make test-tsan— ASAN and TSAN instrumented Rust test suites ran with no sanitizer findings on any test including the new fixtures; themaketargets exit non-zero due to the 2 pre-existing test failures preventing the subsequent C contract stage from runningmake test-jvm— all JVM contracts pass (FFM, C ABI, Netty channel, NativeSslEngine, transport ABI);NativeSslInteropTestfails due toposix_spawnEACCES on/usr/bin/openssl(container restriction, unrelated to this fix)New tests added:
native_h2_free_flow_control_stalled_stream_resets_under_indefinite_poll— regression test that frees a flow-control-stalled stream, issues a singlepoll(u64::MAX), and asserts the poll returns within ~7 s (the 5 s deadline fires without any external wake). A watchdogdriverWakeat 8 s catches a broken fix.native_h2_cooperative_peer_retires_normally_under_indefinite_poll— verifies a cooperative peer (small body that fits the flow-control window) retires normally and that an indefinite poll with no armed deadline blocks indefinitely (no busy-loop).next_deadline_returns_none_without_armed_stalls_and_earliest_with_many— unit test verifyingnext_deadlinereturnsNonewhen no h2 streams have the stall detector armed.Automatic Fixes PRs can be configured here.