Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/scripts/windows/build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ if not exist "%SDK_RUNNER%" (
exit /b 3
)

for /f "delims=" %%T in ('call .github\scripts\windows\find-vs-toolset.bat %PHP_BUILD_CRT%') do set "VS_TOOLSET=%%T"
echo Got VS Toolset %VS_TOOLSET%
cmd /c %SDK_RUNNER% -s %VS_TOOLSET% -t .github\scripts\windows\build_task.bat
cmd /c %SDK_RUNNER% -t .github\scripts\windows\build_task.bat
if %errorlevel% neq 0 exit /b 3

exit /b 0
22 changes: 6 additions & 16 deletions .github/scripts/windows/find-target-branch.bat
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
@echo off

rem Pin the PHP SDK dependency series to 8.5 (vs17).
rem
rem We build php-src's dev tip (true-async == 8.6-dev) on the windows-2022
rem runner, i.e. the vs17 / VS 2022 toolchain. php.net's deps server only
rem publishes the bleeding edge (branches "8.6" and "master") for the vs18 /
rem VS 2026 toolchain -- there is no vs17 build of those. The newest series
rem that ships vs17 dependencies is 8.5, and those libs (openssl, libxml2,
rem ...) build the 8.6 tip fine: the series tracks the toolchain, not the PHP
rem minor. Asking for "8.6"/"master" under --crt vs17 fails with
rem "CRT 'vs17' doesn't match any available for branch ...".
rem
rem Bump this to 8.6 once php.net publishes packages-8.6-vs17-*, or switch the
rem runner+PHP_BUILD_CRT to vs18 to follow the dev-tip deps directly.
rem (Old logic derived "8.<minor>" and remapped a hardcoded 8.5 -> master;
rem it broke when php-src went 8.6 and the dev deps moved to vs18.)
set BRANCH=8.5
for /f "usebackq tokens=3" %%i in (`findstr PHP_MAJOR_VERSION main\php_version.h`) do set BRANCH=%%i
for /f "usebackq tokens=3" %%i in (`findstr PHP_MINOR_VERSION main\php_version.h`) do set BRANCH=%BRANCH%.%%i

if /i "%BRANCH%" equ "8.5" (
set BRANCH=master
)
3 changes: 1 addition & 2 deletions .github/scripts/windows/test.bat
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ if not exist "%SDK_RUNNER%" (
exit /b 3
)

for /f "delims=" %%T in ('call .github\scripts\windows\find-vs-toolset.bat %PHP_BUILD_CRT%') do set "VS_TOOLSET=%%T"
cmd /c %SDK_RUNNER% -s %VS_TOOLSET% -t .github\scripts\windows\test_task.bat
cmd /c %SDK_RUNNER% -t .github\scripts\windows\test_task.bat
if %errorlevel% neq 0 exit /b 3

exit /b 0
6 changes: 3 additions & 3 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ env:
jobs:
windows:
name: WINDOWS_X64_ZTS_RELEASE
runs-on: windows-2022
runs-on: windows-2025-vs2026
timeout-minutes: 60
env:
PHP_BUILD_CACHE_BASE_DIR: C:\build-cache
PHP_BUILD_OBJ_DIR: C:\obj
PHP_BUILD_CACHE_SDK_DIR: C:\build-cache\sdk
PHP_BUILD_SDK_BRANCH: php-sdk-2.3.0
PHP_BUILD_CRT: vs17
PHP_BUILD_SDK_BRANCH: php-sdk-2.7.1
PHP_BUILD_CRT: vs18
PLATFORM: x64
THREAD_SAFE: "1"
INTRINSICS: AVX2
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **hq-interop (HTTP/0.9-over-QUIC) for the interop matrix (#80).** A second QUIC
ALPN, `hq-interop`, served straight off the transport (no nghttp3): a raw bidi
stream `GET <path>` returns the file bytes + FIN from `setHttp3HqDocroot()`.
Lets the quic-interop-runner reach the server for the whole transport matrix
(transfer/multiplexing/migration/loss), which it negotiates over hq, not h3.
`h3` stays preferred when a peer offers both; the h3 path is unchanged.

- **HTTP/3 transport reactor pool (experimental, #80).** Behind
`TRUE_ASYNC_SERVER_REACTOR_POOL=1` + `setWorkers(2+)`: dedicated C reactors own the
QUIC sockets (no PHP on the transport thread), hand parsed requests to PHP workers
by pointer, and serve responses back over a non-blocking reverse channel; static
files are served on the reactor. Adds CID steering (owner-reactor id encoded in the
connection id, forwarding migrated clients across the split — #72) and a
migration-storm guard that sheds clients rebinding past a rate cap. Dispatch is
reactor-paired: a connection sticks to one of its reactor's workers and spills to a
less-loaded worker when its home backs up or dies. Off by default.
- Lock-free inter-thread message queue primitive (#81): bounded MPSC/SPSC C-ABI
wrappers over moodycamel (`thread_queue`) plus a reactor-integrated MPSC mailbox
(`thread_mailbox`) that wakes the consumer's loop via a trigger event with
Expand Down
12 changes: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ set(CORE_SUBSYSTEM_SOURCES
# Inter-thread message queue (issue #81): C++ moodycamel wrapper + C reactor glue.
src/core/thread_queue.cc
src/core/thread_mailbox.c
# Reactor thread pool (issue #80): pure-C transport reactors on the ThreadPool.
src/core/reactor_pool.c
src/core/reactor_pool_test_hooks.c
# Flat response marshalling type (issue #80, D3): worker -> reactor response.
src/core/response_wire.c
# Worker-side request dispatch (issue #80, B1b/D7): request ptr -> handler -> response.
src/core/worker_dispatch.c
# Worker inbox (issue #80, B2): per-worker request mailbox + dispatch drain.
src/core/worker_inbox.c
# Worker registry (issue #80, B3): atomic table of per-worker inboxes.
src/core/worker_registry.c
)

# TLS sources — only compiled when OpenSSL is present. Both files
Expand Down Expand Up @@ -231,6 +242,7 @@ if(ENABLE_HTTP3)
src/http3/http3_callbacks.c
src/http3/http3_io.c
src/http3/http3_packet.c
src/http3/http3_steer.c
src/http3/http3_stream.c
src/http3/http3_stream_pool.c
src/http3/http3_static_response.c
Expand Down
24 changes: 23 additions & 1 deletion config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ PHP_ARG_ENABLE([http3],
[yes],
[no])

PHP_ARG_ENABLE([http-server-test-hooks],
[whether to compile internal test hooks],
[AS_HELP_STRING([--enable-http-server-test-hooks],
[Compile internal C test hooks (e.g. reactor-pool self-test). For test/CI builds only; never enable for release.])],
[no],
[no])

PHP_ARG_WITH([openssl],
[for OpenSSL TLS support],
[AS_HELP_STRING([--with-openssl@<:@=DIR@:>@],
Expand Down Expand Up @@ -492,6 +499,12 @@ if test "$PHP_HTTP_SERVER" != "no"; then
src/core/async_plain_event.c
src/core/thread_queue.cc
src/core/thread_mailbox.c
src/core/reactor_pool.c
src/core/reactor_pool_test_hooks.c
src/core/response_wire.c
src/core/worker_dispatch.c
src/core/worker_inbox.c
src/core/worker_registry.c
src/http1/http_parser.c
src/http1/http1_stream.c
src/http1/http1_sendfile.c
Expand Down Expand Up @@ -585,6 +598,7 @@ if test "$PHP_HTTP_SERVER" != "no"; then
src/http3/http3_io.c
src/http3/http3_callbacks.c
src/http3/http3_dispatch.c
src/http3/http3_steer.c
src/http3/http3_static_response.c
src/http3/http3_stream.c
src/http3/http3_stream_pool.c
Expand Down Expand Up @@ -613,9 +627,17 @@ if test "$PHP_HTTP_SERVER" != "no"; then
done
CFLAGS="$SAVE_CFLAGS"

dnl Test-only C hooks (reactor-pool self-test, ...). Gated behind a define so
dnl the hook is absent from a release build. Test/CI builds opt in.
HTTP_SERVER_TEST_HOOKS_FLAG=""
if test "$PHP_HTTP_SERVER_TEST_HOOKS" = "yes"; then
AC_MSG_NOTICE([http_server: internal test hooks ENABLED — do not ship this build])
HTTP_SERVER_TEST_HOOKS_FLAG="-DHTTP_SERVER_TEST_HOOKS=1"
fi

dnl Create extension. The trailing "cxx" arg makes the shared module link
dnl through $(CXX) so the C++ TU's runtime (libstdc++) is pulled in.
PHP_NEW_EXTENSION(true_async_server, $http_server_sources, $ext_shared,, -Wall -Wextra -Wno-unused-parameter $HTTP_SERVER_HARDENING, cxx)
PHP_NEW_EXTENSION(true_async_server, $http_server_sources, $ext_shared,, -Wall -Wextra -Wno-unused-parameter $HTTP_SERVER_HARDENING $HTTP_SERVER_TEST_HOOKS_FLAG, cxx)
PHP_SUBST(TRUE_ASYNC_SERVER_SHARED_LIBADD)

dnl Add include paths
Expand Down
6 changes: 6 additions & 0 deletions config.w32
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ if (PHP_TRUE_ASYNC_SERVER == "yes") {
"async_plain_event.c " +
"thread_queue.cc " +
"thread_mailbox.c " +
"reactor_pool.c " +
"reactor_pool_test_hooks.c " +
"response_wire.c " +
"worker_dispatch.c " +
"worker_inbox.c " +
"worker_registry.c " +
"http_connection.c " +
"http_connection_tls.c " +
"http_protocol_handlers.c " +
Expand Down
51 changes: 51 additions & 0 deletions docs/CODING_STANDARDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,57 @@ eventfd indirection actually serves. Do not use it as a defer mechanism
for in-thread work; microtasks are cheaper (no syscall) and clearer
about intent.

### 1.5 The transport reactor stays strictly non-blocking (HTTP/3)

In HTTP/3 the entire QUIC transport — ACK generation, loss detection,
RTT/PTO, pacing, idle timers — runs in userspace **on the reactor thread**
(ngtcp2 has no internal threads). The reactor *is* the ACK clock. A
synchronous CPU burst or any blocking call on that thread delays ACKs for
**every** live connection by its full duration, which inflates the peers'
RTT/PTO, stalls cwnd, and reintroduces head-of-line blocking at the
transport layer. (On TCP the kernel ACKs independently, so this rule is
QUIC-specific — but the H3 path shares code with H1/H2, so apply it
wherever code can run on the H3 reactor.)

**Rule.** Code reachable on the H3 reactor thread — the `recvmmsg`
poll-cb (`src/http3/http3_listener.c` `http3_listener_poll_cb`), every
ngtcp2/nghttp3 callback (they run inside `ngtcp2_conn_read_pkt`), the
`drain_out` send loop (`src/http3/http3_io.c`), timer fires, and the
coroutine **dispose**/commit tail (`src/http3/http3_dispatch.c`) — must
not perform an unbounded synchronous span:

- **No blocking syscalls.** No sync DB, sync file read, blocking
`connect`/`getaddrinfo`, or `sleep`. I/O goes through the async API so
it yields.
- **No unbounded CPU without a yield.** Large gzip/brotli/zstd, large
serialize, a big `smart_str` build, a wide hash/sort over
attacker-sized input — none of these belong inline on a callback or in
the dispose commit. Cap it, or move it onto the PHP worker (a handler
coroutine that `await`s; the reactor/worker split keeps response
rendering — including compression — off the transport reactor, see
`docs/PLAN_REACTOR_POOL.md`).
- **Every loop over peer-controlled counts has a cap.** Follow the
existing precedents: `H3_DRAIN_ITER_CAP` (drain), `HTTP3_MAX_BODY_BYTES`
(body assembly), the `recvmmsg` batch cap (poll-cb).

The PHP **handler** runs in its own coroutine and *may* `await` — that is
the sanctioned place for real work. But a CPU-bound handler that never
awaits monopolises the reactor exactly like inline reactor code; "it's in
a coroutine" is not a yield. When a handler must do heavy CPU, it has to
reach an await (chunk + yield), not run it in one synchronous span.

The buffered-response compression in `http3_stream_submit_response`
(`src/http3/http3_callbacks.c`) runs synchronously in dispose context — a
current example of inline CPU on the reactor. The reactor/worker split
moves response rendering onto the PHP worker; until then, keep buffered
bodies modest and prefer the streaming path for large ones.

**Watchdog.** The reactor self-times each tick and each timer fire and
exports `reactor_*` counters via `HttpServer::getStats()` (budget
`PHP_HTTP3_REACTOR_BUDGET_MS`, default 10 ms < `max_ack_delay` 25 ms); a
budget overrun logs `WARN h3.reactor.slow_tick`. If a change makes
`reactor_slow_ticks` / `reactor_max_tick_ns` climb, it violated this rule.

---

## 2. Branch prediction: `EXPECTED` / `UNEXPECTED`
Expand Down
Loading
Loading