Skip to content

11 built in worker pool inside httpserver replace user level threadpool boilerplate - #12

Merged
EdmondDantes merged 5 commits into
mainfrom
11-built-in-worker-pool-inside-httpserver-replace-user-level-threadpool-boilerplate
May 6, 2026
Merged

EdmondDantes merged 5 commits into
mainfrom
11-built-in-worker-pool-inside-httpserver-replace-user-level-threadpool-boilerplate

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

No description provided.

Adds HttpServerConfig::setWorkers(N) / getWorkers(). When workers > 1,
HttpServer::start() builds an Async\ThreadPool(N), submits N copies of
a worker stub closure that transfers $this to each worker thread, and
awaits every worker's standalone start(). Each worker re-binds the
configured listeners; the kernel load-balances accept() via
SO_REUSEPORT.

The pool stub is a tiny user-land closure built via zend_eval_string —
internal methods have no op_array, so the array-callable form
[$this, 'start'] crashes the cross-thread snapshot serialiser.

Default setWorkers(1) keeps current behaviour bit-for-bit.

Caveats (documented):
- $server->stop() on the pool-parent throws: Async\ThreadPool::cancel()
  doesn't wake workers suspended on their own server's wait event.
  Cross-thread shutdown is a follow-up.
- SO_REUSEPORT is Linux/BSD-only; Windows libuv falls back to single
  accept thread.

phpt: tests/phpt/server/core/020-builtin-worker-pool.phpt verifies the
spin-up + serve path.
conn->drain_not_before_ns is computed from zend_hrtime() (CLOCK_MONOTONIC_RAW
on Linux). The dispose path's fallback used http_now_coarse_ns()
(CLOCK_MONOTONIC_COARSE) when CoDel and telemetry were both disabled —
the two clocks drift by minutes after suspend / NTP slewing
(observed: ~35 min on a long-running WSL2 VM), so the drain check
mis-fired on the first response.

Same fix in the H2 commit path, which had the identical pattern.
H3 already used zend_hrtime() directly.

tests/phpt/server/h1/010-drain-proactive-age-h1.phpt was failing on
machines with non-trivial RAW/COARSE skew; passes consistently now.
Replaces the placeholder Async\ThreadPool/zend_eval_string-driven
worker pool path with a direct C-handler submission:

* New pool_worker_handler — runs on the worker thread, LOAD's the
  parent-transferred HttpServer zval, calls $server->start(),
  releases worker-local zval. No PHP closure, no op_array snapshot.

* One persistent shell per worker, allocated as a single pemalloc'd
  array on the parent server. Lifetime is the parent's:
  http_server_free releases the array. Snapshot deep-copy isn't
  concurrency-safe, so workers cannot share a single shell.

* Parent suspends on a wait_event; each worker's completion
  callback decrements a pending counter and notifies when it hits
  zero. Mid-loop submit failure marks remaining slots as already
  done so previously-submitted workers' callbacks land on a still
  -valid state — no UAF on partial failure.

* in_pool_mode bool replaces the previous worker_pool zval —
  parent in pool mode has no listen events, so stop() refuses
  with a clear message until cross-thread shutdown lands.

Requires php-src true-async / true-async-stable + php-async
companion commits with the new submit_internal API and top-level
transfer/load helpers.
If submit_internal fails on the very first iteration we end up with
pending=0 and no callbacks registered to ever notify all_done — the
parent's SUSPEND() would deadlock. Gate the suspend on pending > 0;
when nothing got submitted, just propagate the failure through the
return code.
…anual layout

multi-worker.php is now the recommended preforking example — uses
HttpServerConfig::setWorkers(N), one HttpServer in the script, the
parent's start() blocks on its internal pool. No more user-level
Async\ThreadPool boilerplate.

The previous user-driven layout is preserved as
multi-worker-manual.php — it stays useful when you need a
per-worker init step that runs before listeners come up
(opcache warm-up, DB pool bootstrap, custom fixtures), since
setWorkers() doesn't expose a worker-init hook yet.

README quick-start shows setWorkers(N) opt-in; USAGE.md
multi-worker section + 'Where to look next' updated to reference
both example files.
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Coverage

Total lines: 80.89% → 80.10% (-0.79 pp)

File Baseline Current Δ Touched
src/core/http_connection.c 77.65% 77.65% +0.00 pp ●
src/http2/http2_strategy.c 75.88% 75.88% +0.00 pp ●
src/http3/http3_callbacks.c 79.45% 78.86% -0.59 pp
src/http_server_class.c 82.74% 76.42% -6.32 pp ●
src/http_server_config.c 86.60% 85.71% -0.88 pp ●

❌ Regression in touched files (> 1.0 pp drop)

  • src/http_server_class.c dropped -6.32 pp

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

@EdmondDantes
EdmondDantes merged commit 3966952 into main May 6, 2026
5 checks passed
@EdmondDantes
EdmondDantes deleted the 11-built-in-worker-pool-inside-httpserver-replace-user-level-threadpool-boilerplate branch May 6, 2026 14:52
EdmondDantes added a commit that referenced this pull request May 8, 2026
  #8  Drop the prefixed[PATH_MAX] scratch in http_static_path_resolve.
      validate_segments already tolerates either a leading '/' or none.

  #9  http_static_path_is_hidden no longer memcpy's `relative` into a
      4 KiB scratch buffer — http_static_path_resolve writes the NUL
      right after `relative` inside out_buf, so fnmatch can read it
      directly.

  #12 MIME header comment now describes override-wins semantics
      (mime_overrides consulted before the built-in table) instead of
      the inverted version.

  #13 ss_state_t.fs_path drops from char[PATH_MAX] to emalloc'd
      char *.  New ss_state_free helper centralises ownership.  With
      10 K concurrent in-flight static requests this saves up to 40
      MiB of working set.

  #15 canonicalise_root_directory rejects realpath() == "/" up front;
      previously a root="/" StaticHandler would silently serve nothing
      because resolved_under_root needs canonical[root_len] in {NUL,'/'}.
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.

Built-in worker pool inside HttpServer (replace user-level ThreadPool boilerplate)

1 participant