feat: built-in hot-reload triggers — file watcher + SIGHUP (#93)#99
Merged
Conversation
…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.
Contributor
CoverageTotal lines: 75.64% → 74.51% (-1.12 pp)
❌ Regression in touched files (> 1.0 pp drop)
Add |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 recursiveAsync\FileSystemWatcherper 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=1the C reactors keep routing through worker inboxes registered for the pool's lifetime (worker_inbox_freeassumes «producers have quiesced», andworker_registry_removedoes not exist) — rotating workers under live reactors →zend_mm_heap corruptedin 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.