Skip to content

reactor: the wait's timeout replaces the 250 ms timer, and wakes for a due QUIC deadline - #272

Merged
MDA2AV merged 1 commit into
mainfrom
fix/quic-wait-replaces-timer
Oct 4, 2026
Merged

MDA2AV merged 1 commit into
mainfrom
fix/quic-wait-replaces-timer

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

An idle reactor sleeps in io_uring_enter until a completion arrives, and the only one it can count on is the 250 ms ticker. So a QUIC retransmit, ACK or pacing step due in a few ms waited for the next tick. #267 made the deadline correct; nothing woke the loop for it.

This makes the wait itself the timer: it is bounded by the next ticker run (250 ms) or the earliest engine deadline, whichever is sooner. The tickers run from the loop once their interval is up, checked every pass, because a busy reactor's wait never runs out. The IORING_OP_TIMEOUT that used to drive them, its KindTimer completion and its native timespec are gone.

Chosen over #271, which kept the timer and only bounded the wait for QUIC.

Tests

  • quic/timer: an idle reactor fires an engine deadline when it is due, not at the next tick: the answer is lost and the peer stays silent, so the retransmit timer fires again and again; each firing is timed against the engine's own ns deadline.
    • main: up to 226-237 ms late ([146, 233, 22, 44, 89], [144, 226, 13, 26, 52] ms)
    • this PR: under 1.1 ms
  • quic/timer: a reactor whose last connection is gone does not keep waking for it: nothing resets the tracked deadline when the last connection leaves, so the wait ignores it then. Without that guard the empty reactor woke 996 times/s.
  • tcp/read: a quiet connection is reaped while another keeps the reactor busy: the sweeps must not wait for an idle moment. With the tickers run only when the wait times out, the quiet connection was never reaped (644,301 exchanges on the other one over 4 s).
  • quic/timer: a deadline that is already due is reported as due… needed the loop to notice a deadline 2 ms late, which it no longer does; any amount past now counts (the clock only moves forward).

All suites pass, pending lists unchanged from main: E2E 234, Unit 60, Chaos 47, Http 44, Tls 151 (the 7 kTLS tests skip without sudo), File 4.

Bench

Interleaved: 5 rounds of 10 s per sample in rotating order, 2 reactors pinned to the P-cores. control is a second build of main, so its delta is the noise floor. Medians against main, req/s · CPU per request:

sample main this PR #271 control (main again)
Tcp/Raw (h1) 883,722/s · 2.285 µs +4.3% · −4.0% +1.3% · −1.3% +3.7% · −3.5%
Http3/Nghttp3Buffered (h3, 16 conns) 782,765/s · 2.552 µs +1.3% · −1.3% +0.9% · −0.8% −0.7% · +0.7%
Tls/OpenSsl (h1s) 365,622/s · 5.533 µs +0.7% · −0.7% +0.1% · −0.1% +2.3% · −2.2%

Every delta is inside the control's own spread, so neither design costs anything measurable under load. Tcp/Raw rerun over 10 rounds, within-round median vs main: this PR +0.3%, #271 −0.8%, control −0.9% - the +4.3% above was noise. Idle reactors wake 4.0-4.2 times/s on main and both PRs alike (the 250 ms tick), for TCP and for QUIC after a 1.35M-request h3 burst has ended.

Size: src +87 −72 in 11 files.

…a due QUIC deadline

The loop's wait is bounded by the next ticker run and the earliest engine deadline, so
an idle reactor fires a retransmit, ACK or pacing step when it is due. The tickers run
from the loop once their interval is up - checked every pass, since a busy reactor's wait
never runs out - and the IORING_OP_TIMEOUT that used to drive them is gone.
@MDA2AV
MDA2AV merged commit dacf684 into main Oct 4, 2026
1 check passed
@MDA2AV MDA2AV mentioned this pull request Oct 4, 2026
@MDA2AV
MDA2AV deleted the fix/quic-wait-replaces-timer branch October 5, 2026 15:28
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.

1 participant