Skip to content

feat: built-in hot-reload triggers — file watcher + SIGHUP (#93)#99

Merged
EdmondDantes merged 2 commits into
mainfrom
93-hotreload-triggers
Jul 3, 2026
Merged

feat: built-in hot-reload triggers — file watcher + SIGHUP (#93)#99
EdmondDantes merged 2 commits into
mainfrom
93-hotreload-triggers

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

Completes the #93 trigger layer on top of HttpServer::reload() (#97):

enableHotReload — dev trigger

$config->enableHotReload(['/app/src'], ['php'], 300, 2000) — the pool parent arms one recursive Async\FileSystemWatcher per path (engine-side debounce collapses change bursts into one event); each delivered event invalidates the watched trees in opcache and rotates the pool. Editing a watched file is now the only action needed: the same port serves the new code (test 053 proves v1→v2 with zero manual calls).

enableReloadOnSignal — prod trigger

One persistent SIGHUP event armed for the whole pool run — no unarmed window between deliveries (kill -HUP <pid> → rotation). Throws on Windows.

Both orchestrators are internal main-scope coroutines on the parent, torn down with the pool (watchers closed, signal waiter woken); everything logs through the parent's http_log (reload.watch armed, reload.trigger source=watcher|sighup, …).

Found & guarded: reactor-pool reload corrupts the heap

The new h3-under-reload test caught a real one: with TRUE_ASYNC_SERVER_REACTOR_POOL=1 the C reactors keep routing through worker inboxes registered for the pool's lifetime (worker_inbox_free assumes «producers have quiesced», and worker_registry_remove does not exist) — rotating workers under live reactors → zend_mm_heap corrupted in the reactor threads. Until inbox rotation is designed (#80 follow-up), reload() now refuses loudly in that mode; h3/045 locks the guard in and proves the refusal is side-effect free.

Tests

053 (watcher e2e), 054 (SIGHUP), websocket/034 (reload with a live WS connection: old conn retired after the short drain grace, fresh cohort serves WS), h3/045 (reactor-pool guard). Full sweep: 277 passed, 0 failed.

…leReloadOnSignal (SIGHUP) (#93)

- HttpServerConfig::enableHotReload(watchPaths, extensions, debounceMs,
  maxHoldMs): the pool parent arms one recursive Async\FileSystemWatcher
  per path (engine-side debounce collapses change bursts); each delivered
  event invalidates the watched trees in opcache and rotates the pool via
  HttpServer::reload(). Config fields are parent-only, never snapshotted.
- HttpServerConfig::enableReloadOnSignal(): a persistent SIGHUP event armed
  for the whole pool run (no unarmed window between deliveries), each
  delivery is one rotation. Throws on Windows.
- Orchestrators are internal main-scope coroutines on the parent; teardown
  closes the watchers (ends their iterators) and notifies the signal waiter,
  which observes hot_reload_stopping and disposes its own event.
- Logged through the parent's http_log: reload.watch armed/failed,
  reload.signal armed, reload.trigger source=watcher|sighup.
- reload() now refuses under TRUE_ASYNC_SERVER_REACTOR_POOL=1: reactors
  route through worker inboxes registered for the pool's lifetime and there
  is no registry unregister yet — retiring workers under live reactors
  corrupted the heap (found by the new h3 test). Guard + test document the
  limit until inbox rotation lands (#80 follow-up).
- tests: 053 watcher e2e (file edit -> new code served, no manual reload),
  054 SIGHUP rotation, websocket/034 reload with a live WS connection
  (old conn retired, fresh cohort serves WS), h3/045 reactor-pool guard.
  Full sweep: 277 passed, 0 failed.
…llision flake class (#93)

206 tests used 'BASE + getmypid() % N' with massively overlapping ranges
(several %1000 spans, six tests sharing one literal base) while the server
binds with SO_REUSEPORT — so under run-tests -j any two concurrent tests
could silently share a port and split each other's traffic. That is the
mechanism behind the h2/012 flake fixed in #98 and the websocket/024
backpressure flake seen on this PR's first CI round (its base 19790+%100
sits in the middle of the crowd).

All 206 files now take tas_free_port(); tests that derive sibling ports
as $port+N use the new tas_free_port_span(N+1), which holds the whole
span before releasing it. Full suite at -j8, twice: 277 passed, 0 failed.
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Coverage

Total lines: 75.64% → 74.51% (-1.12 pp)

File Baseline Current Δ Touched
src/http3/http3_listener.c 66.35% 66.19% -0.16 pp
src/http_server_class.c 59.53% 53.08% -6.46 pp
src/http_server_config.c 93.01% 89.83% -3.18 pp

❌ Regression in touched files (> 1.0 pp drop)

  • src/http_server_class.c dropped -6.46 pp
  • src/http_server_config.c dropped -3.18 pp

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

@EdmondDantes EdmondDantes merged commit d9251ff into main Jul 3, 2026
8 checks passed
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