Skip to content

feat(http3): transport reactor pool + CID steering + hq-interop endpoint (#80)#90

Merged
EdmondDantes merged 9 commits into
mainfrom
80-http3-dont-block-the-transportreactor-thread-with-business-logic-ack-timing-budget
Jun 26, 2026
Merged

feat(http3): transport reactor pool + CID steering + hq-interop endpoint (#80)#90
EdmondDantes merged 9 commits into
mainfrom
80-http3-dont-block-the-transportreactor-thread-with-business-logic-ack-timing-budget

Conversation

@EdmondDantes

@EdmondDantes EdmondDantes commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

What

The #80 work: split the HTTP/3 transport from PHP execution (reactor pool),
add CID steering + a migration-storm guard, fix rotated-DCID routing, and
add a quic-interop endpoint with an hq-interop (HTTP/0.9-over-QUIC) shim.

49 commits, ~+8.7k/-0.7k LOC. The hot h3 path stays byte-for-byte unchanged
(all new behaviour is gated/parallel); the full h3 phpt suite is green and
ASan-clean throughout.

Highlights

Transport reactor pool (D1–D8, gated TRUE_ASYNC_SERVER_REACTOR_POOL=1 + setWorkers(2+))

  • Dedicated C reactors own the QUIC sockets — no PHP on the transport thread.
  • Parsed requests cross to PHP workers by pointer (one http_request_t,
    persistent allocation domain, flag-aware accessors); responses return over
    a non-blocking reverse channel. Buffered GET/POST + static served on the
    reactor. Reactor-paired sticky dispatch with load spill (D5).

CID steering (D6) + robustness

  • Owner-reactor id encoded in the connection id; stray (migrated) datagrams
    forwarded to the owner reactor. Migration-storm guard sheds connections
    that rebind past a rate cap. Known limitation documented: a circular
    path-validation deadlock under pathological back-to-back migrations.
  • Fix: register issued NEW_CONNECTION_ID CIDs in the conn_map so rotated
    DCIDs route (RFC 9000 §5.1).

Interop endpoint + hq-interop shim

  • Second QUIC ALPN hq-interop served straight off the transport (no
    nghttp3): raw GET <path> -> file bytes + FIN from setHttp3HqDocroot(),
    zero-copy via mmap, correct FIN incl. empty bodies. h3 stays preferred.
  • quic-interop-runner endpoint (tests/interop/quic/, build-from-source
    Dockerfile).

Testing

  • Full h3 phpt suite 44/44 on release AND under ASan (php-asan2 +
    instrumented .so): zero use-after-free / overflow / double-free across the
    reactor split, CID steering, and the hq path.
  • hq validated byte-exact for 0/1/5K/16K/128K/512K, multiplexing, traversal,
    and under 5% packet loss (netem) — plus a live quic-go interop client
    (downloaded files, exit 0).
  • Real HTTP/3 confirmed with an independent aioquic h3 client (:status 200,
    byte-exact 5K + 512K).

Known follow-ups (not blocking)

  • D4 generationed handle / two-sided cancel — premature until CID work needs
    it (raw ptr proven safe today).
  • quic-interop-runner green verdicts need a native Linux host; the ns-3
    simulator's packet capture is incomplete under Docker Desktop + WSL2.
  • Optional: export QLOGDIR / SSLKEYLOGFILE for richer runner verification.

Refs #80.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Coverage

Total lines: 81.42% → 75.80% (-5.62 pp)

File Baseline Current Δ Touched
src/core/http_connection.c 70.08% 68.09% -1.99 pp
src/core/http_connection.h 81.82% 81.82% +0.00 pp
src/core/reactor_pool.c 0.00% 0.00% +0.00 pp
src/core/reactor_pool_test_hooks.c 0.00% 100.00% +100.00 pp
src/core/response_wire.c 0.00% 0.00% +0.00 pp
src/core/thread_mailbox.c 0.00% 0.00% +0.00 pp
src/core/tls_layer.c 76.64% 76.64% +0.00 pp
src/core/worker_dispatch.c 0.00% 0.00% +0.00 pp
src/core/worker_inbox.c 0.00% 0.00% +0.00 pp
src/core/worker_registry.c 0.00% 0.00% +0.00 pp
src/http1/http_parser.c 83.17% 83.52% +0.35 pp
src/http2/http2_session.c 87.40% 87.36% -0.03 pp
src/http3/http3_callbacks.c 80.43% 61.76% -18.67 pp
src/http3/http3_connection.c 82.27% 80.56% -1.71 pp
src/http3/http3_dispatch.c 80.00% 62.28% -17.72 pp
src/http3/http3_io.c 89.27% 81.45% -7.82 pp
src/http3/http3_listener.c 74.34% 66.35% -8.00 pp
src/http3/http3_packet.c 91.21% 91.00% -0.21 pp
src/http3/http3_steer.c 0.00% 5.00% +5.00 pp
src/http3/http3_stream.c 95.56% 76.12% -19.44 pp
src/http_request.c 86.36% 81.40% -4.96 pp
src/http_server.c 94.64% 94.69% +0.05 pp
src/http_server_class.c 67.80% 62.16% -5.64 pp
src/http_server_config.c 95.29% 93.63% -1.66 pp
src/send_file.c 87.09% 87.13% +0.04 pp
src/static/http_static.c 88.70% 88.75% +0.05 pp

❌ Regression in touched files (> 1.0 pp drop)

  • src/core/http_connection.c dropped -1.99 pp
  • src/http3/http3_callbacks.c dropped -18.67 pp
  • src/http3/http3_connection.c dropped -1.71 pp
  • src/http3/http3_dispatch.c dropped -17.72 pp
  • src/http3/http3_io.c dropped -7.82 pp
  • src/http3/http3_listener.c dropped -8.00 pp
  • src/http3/http3_stream.c dropped -19.44 pp
  • src/http_request.c dropped -4.96 pp
  • src/http_server_class.c dropped -5.64 pp
  • src/http_server_config.c dropped -1.66 pp

Add [coverage-drop-ok] to a commit message in this PR to override.

…int (#80)

Split the HTTP/3 transport from PHP execution and add the interop endpoint.

Reactor pool (D1-D8, gated TRUE_ASYNC_SERVER_REACTOR_POOL=1 + setWorkers(2+)):
dedicated C reactors own the QUIC sockets - no PHP on the transport thread.
Parsed requests cross to PHP workers by pointer (one http_request_t,
persistent allocation domain, flag-aware accessors); responses return over a
non-blocking reverse channel. Buffered GET/POST and static are served on the
reactor; reactor-paired sticky dispatch with load spill (D5).

CID steering (D6): owner-reactor id encoded in the connection id, stray
(migrated) datagrams forwarded to the owner; migration-storm guard sheds
connections rebinding past a rate cap. Fix: register issued NEW_CONNECTION_ID
CIDs in the conn_map so rotated DCIDs route (RFC 9000 5.1).

Interop: second QUIC ALPN hq-interop served straight off the transport (no
nghttp3) - raw GET <path> -> file bytes + FIN from setHttp3HqDocroot(),
zero-copy via mmap, correct FIN incl. empty bodies; h3 stays preferred.
quic-interop-runner endpoint with a build-from-source Dockerfile.

Tests: full h3 phpt suite 44/44 on release and under ASan (no UAF / overflow /
double-free) across the split, CID steering and the hq path; hq validated
byte-exact (0/1/5K/16K/128K/512K), multiplexing, traversal, and under 5%
packet loss; real HTTP/3 confirmed with an independent aioquic h3 client.
Fuzz: weak stub for http_request_init_headers so the parser/h2 fuzz targets link.

Refs #80.
@EdmondDantes EdmondDantes force-pushed the 80-http3-dont-block-the-transportreactor-thread-with-business-logic-ack-timing-budget branch from 07ce14c to c92b4ea Compare June 26, 2026 15:10
….c (#80)

These are HTTP_SERVER_TEST_HOOKS-gated entry points the phpt suite calls to
drive reactor-pool / worker internals directly — not a unit test. The old
name read like a stray test living in src/core; the new name makes clear it
is test-hook plumbing. Pure rename: file body and build gating unchanged,
config.m4 / config.w32 / CMakeLists updated. reactor_pool phpt 9/9 green.
The rename commit staged only the file move; git add aborted on a stale
pathspec, so config.m4 / CMakeLists.txt / config.w32 still listed the old
reactor_pool_test.c and a fresh build/checkout could not find it. Completes
the rename.
Strip task/PR/phase tags from function bodies (allowed only in module-header
banners per CODING_STANDARDS sec 13a.4), drop decorative WHAT-restating and
"used by X" notes, collapse over-verbose docstrings -- keeping the
load-bearing lifetime/threading/UAF/security invariants. Comments only; no
behaviour change. h3 + reactor_pool phpt 53/53.
)

§1: confine the hq-interop file open to the docroot via openat2(RESOLVE_BENEATH)
with a realpath+containment fallback, closing the realpath()->open() TOCTOU
window. Gate the POSIX mmap path under #ifndef PHP_WIN32 (Windows stub returns
false; munmap site guarded too) so http3 compiles on Win32.

Cleanups across the #80 reactor/worker + H3 code:
- dead internal NULL-checks -> ZEND_ASSERT (worker inbox/dispatch/registry,
  http3 packet/listener/connection/dispatch/callbacks; both coroutine disposes).
- const on write-once locals/params and read-only accessors.
- EXPECTED/UNEXPECTED hints on cold reject/overflow/alloc-fail branches only.
- local/param renames for clarity; drop a stray scope block; magic 12 ->
  sizeof; refresh a stale "reactors carry no transport" comment.

Build clean (0 warnings), phpt 240/240.

Claude-Session: https://claude.ai/code/session_019jfu3UHdtfatCpeAM69zeQ
)

PHP 8.6's php-sdk-binary-tools (php-sdk-2.7.1) no longer ships vs17 deps, so
the old config failed at the SDK step: "The passed CRT 'vs17' doesn't match any
available for branch '8.6'". Mirror the true-async/releases workflow, whose
Windows job is green: runner windows-2025-vs2026 (VS2026 toolset), CRT vs18,
SDK branch php-sdk-2.7.1.

Claude-Session: https://claude.ai/code/session_019jfu3UHdtfatCpeAM69zeQ
…eases) (#80)

After the vs18 bump the build got past the SDK step but died in build.bat:
find-vs-toolset.bat buckets every MSVC >= 14.30 as "vs17" and has no vs18
bucket, so `-s %VS_TOOLSET%` resolved to "ERROR: no toolset found for vs18".
The true-async/releases Windows job (green) doesn't pin the toolset at all —
it runs `%SDK_RUNNER% -t <task>` and lets phpsdk-vs18 select the default.
Mirror that in build.bat and test.bat (find-vs-toolset.bat now unused).

Claude-Session: https://claude.ai/code/session_019jfu3UHdtfatCpeAM69zeQ
…ally (#80)

CI (LINUX_X64_DEBUG_ZTS) intermittently saw cooldown_blocked=0 (expected 1).
Root cause: conn1 was an idle keep-alive (drain_spread defaults to 5s, so it is
not drained at its first commit), so its single conn slot only freed when the
client's 3s read timed out and FIN'd. On a slow/loaded debug build the
slot-free -> listener-resume -> conn2-accept chain then overran conn2's own read
window, so conn2's blocked event had not registered at the fixed-sleep read.

Fix: send `Connection: close` on conn1 so the server frees the slot right after
its response (server-initiated), and poll for the stable terminal state
(epoch/reactive/blocked = 1/1/1) instead of reading once after a fixed sleep.
Verified 12/12 under full 16-core CPU saturation.

Claude-Session: https://claude.ai/code/session_019jfu3UHdtfatCpeAM69zeQ
@EdmondDantes EdmondDantes merged commit 9a23aa0 into main Jun 26, 2026
8 checks passed
@EdmondDantes EdmondDantes deleted the 80-http3-dont-block-the-transportreactor-thread-with-business-logic-ack-timing-budget branch June 26, 2026 19:18
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.

HTTP/3: don't block the transport/reactor thread with business logic (ACK-timing budget)

1 participant