Skip to content

fix(h2): clamp driver poll to armed stall deadline for indefinite timeouts - #52

Open
detail-app[bot] wants to merge 2 commits into
mainfrom
detail/bug-fix/fix-h2-clamp-driver-poll-to-armed-stall-deadline-f-e54bde
Open

detail-app[bot] wants to merge 2 commits into
mainfrom
detail/bug-fix/fix-h2-clamp-driver-poll-to-armed-stall-deadline-f-e54bde

Conversation

@detail-app

@detail-app detail-app Bot commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

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 lazy Instant::now() comparison inside h2_socket::drive(). The stall detector is only evaluated when drive() runs, which happens after elide_transport_driver_poll returns from poll_batch. When an embedder polls with the documented indefinite timeout (timeout_ns = u64::MAX), the driver blocks in proactor.poll(None) with no internal timer or self-wake, so drive() 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 armed freed_at + FLOW_CONTROL_STALL across all streams on a socket; http::next_deadline(state) composes the minimum across all h2 sockets.
  • Both elide_transport_driver_poll and poll_callback clamp the blocking poll timeout to the remaining time until that deadline, so Duration::MAX becomes "sleep until the earliest internal deadline." After poll_batch returns, http::drive_armed_deadlines(state, events) re-drives every socket with an armed stall detector so the lazy Instant::now() verdict in drive() fires even when no I/O woke the driver. The stalled socket is not on the retry queue, so retry_deferred alone cannot reach it.

Closes #47

Validation

Platform: Linux x86_64 (io_uring and epoll backends).

  • cargo build --workspace --locked — passes
  • cargo fmt --all --check — clean
  • cargo clippy --workspace --all-targets --locked -- -D warnings — no warnings
  • cargo check --workspace --no-default-features --locked — passes (external allocator path)
  • cargo test --workspace --doc --locked — passes
  • cargo 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 baseline
  • cargo nextest run -p bemo --test http --locked --no-fail-fast — 70/71 pass; same 1 pre-existing failure
  • ELIDE_TRANSPORT_TEST_BACKEND=epoll cargo nextest run -p bemo --test http --locked — both stall detector tests pass on the epoll/polling backend
  • cargo nextest run -p bemo-ffi --locked — passes
  • make test-miri — passes (installed miri component; library unit tests including the new next_deadline test 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; the make targets exit non-zero due to the 2 pre-existing test failures preventing the subsequent C contract stage from running
  • make test-jvm — all JVM contracts pass (FFM, C ABI, Netty channel, NativeSslEngine, transport ABI); NativeSslInteropTest fails due to posix_spawn EACCES 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 single poll(u64::MAX), and asserts the poll returns within ~7 s (the 5 s deadline fires without any external wake). A watchdog driverWake at 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 verifying next_deadline returns None when no h2 streams have the stall detector armed.

Automatic Fixes PRs can be configured here.

@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.27778% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.79%. Comparing base (da8dbb6) to head (e3b85f3).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
crates/bemo/src/abi/socket.rs 65.00% 7 Missing ⚠️
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     
Flag Coverage Δ
jvm 57.47% <ø> (-0.04%) ⬇️
rust 92.98% <90.27%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@codspeed

codspeed Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚠️ 6 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 117 untouched benchmarks
⏩ 65 skipped benchmarks1


Comparing detail/bug-fix/fix-h2-clamp-driver-poll-to-armed-stall-deadline-f-e54bde (e3b85f3) with main (da8dbb6)

Open in CodSpeed

Footnotes

  1. 65 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Detail Bug] HTTP/2: Freed-stream 5s flow-control stall deadline never fires during indefinite driver poll

0 participants