fix: review follow-ups on the php_server refactor - #2565
Open
nicolas-grekas wants to merge 4 commits into
Open
Conversation
toWorkerOptions() builds a fresh option slice and never appends workerConfig.options, which assignMercureHub() fills with WithWorkerMercureHub(). Start() used to pass w.options directly, so the per-worker Mercure hub is dropped: the field is now written and never read. WithWorkerMercureHub() also appends WithMercureHub() to the worker request options, which is what lets a worker publish from its startup code, outside frankenphp_handle_request(). Request-scoped publishing kept working because the module carries WithMercureHub() in its own request options, which is why no test noticed.
Three lifecycle issues around FrankenPHPApp: - FrankenPHPModule.server is assigned by FrankenPHPApp.Start(), but caddy starts apps by ranging over a map, so the http app can begin serving first. ServeHTTP() dereferenced it unconditionally, turning that window into a nil pointer panic where it used to be a clean ErrNotRunning. Same exposure when registerModules() fails partway through. - Modules append themselves to app.modules during their own Provision(), and app.modules is only cleared by reset(), which runs at the end of Start(). A config that fails to provision therefore leaves ghost modules behind, and since the app is a process singleton they are registered again by the next successful reload. Clear the slice when the app is provisioned instead of only when it starts. - "match" in a global worker block was parsed and then silently ignored, because a global worker is not attached to a php_server and has no set of requests to match against. docs/config.md already documents match as php_server only, so reject it at parse time. No existing Caddyfile in the test suite uses it. The core-side counterpart is in a later commit. Also documents that server_idx is set by the Caddyfile adapter and must not be written by hand, since modules sharing an index share one server.
Server registration: - unregisterServers() only flipped isRegistered, so s.workers, s.workersByPath and s.workersWithRequestMatcher kept the entries added by the previous run. Passing the same *Server to Init() again after a Shutdown() therefore failed with "two workers in a server cannot have the same filename". registerServers() now clears them, and drops the dead workerOpts field while there. - registerServers() also wrote the "server_<idx>" default into s.name, so a later registration at another index kept the stale name. The name given to NewServer() is kept aside and the default resolved anew. - servers were marked registered right after the options were parsed, about a hundred lines before initWorkers() and the thread setup. A request arriving in that window found neither a worker nor a regular thread. Splitting activateServers() out of registerServers() closes it. - servers is now cleared by unregisterServers() and resetGlobals(), like every other worker global. Request matchers: WithWorkerMatcher() on a worker without WithWorkerServerScope() was silently a no-op: initWorkers() only reaches addWorker(), which fills workersWithRequestMatcher, for server-scoped workers, and the fallback server's list stays empty forever. Worse, the worker still landed in globalWorkersByPath and got path-matched, the opposite of what the matcher asked for. newWorker() now rejects the combination, following the same shape as the existing filename and name checks. newWorker() also returned a non-nil worker next to the name-collision error while the filename path returned nil; no caller looked at it.
newWorkerDummyContext() and newContextFromMessage() hardcoded globalLogger even though the worker's server was at hand, so worker startup messages and worker stdout bypassed the per-php_server logger this branch introduces. The module used to append WithRequestLogger() to each worker's request options and that line is gone, so nothing else covered it. go_register_server_variables() merged the prepared env whenever the server had one, but registerPreparedEnv() only runs from go_update_request_info(), which returns early without a request. For a server-scoped extension worker handling a message the merge therefore copied whatever the thread-local prepared env held from an earlier request. Both now agree on the same condition.
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.
Follow-up to #2499, targeting
refactor/phpserver. Four commits, each independently reviewable.Findings from a close read of the branch. Nothing here changes the direction of the refactor; the first two are the ones I would not want to ship.
1. The per-worker Mercure hub is dropped
assignMercureHub()fillsworkerConfig.options, but the newtoWorkerOptions()builds a fresh slice and never appends it, whereStart()used to passw.optionsdirectly. The field is now written and never read.WithWorkerMercureHub()appendsWithMercureHub()to the worker request options, which is what lets a worker publish from its startup code, outsidefrankenphp_handle_request(). Request-scoped publishing kept working because the module carriesWithMercureHub()in its own request options, which is why no test noticed. #2543 extends the same function and inherits the bug.2.
FrankenPHPModule.servercan be nil inServeHTTP()It is assigned by
FrankenPHPApp.Start(), but caddy starts apps by ranging over a map, so the http app can begin serving first. The handler dereferenced it unconditionally, turning that window into a nil pointer panic wheremainreturned a cleanErrNotRunning.Two neighbouring lifecycle issues are fixed in the same commit: modules left in
app.modulesby a config that failed to provision are registered again by the next reload (the app is a process singleton andreset()only runs at the end ofStart()), andmatchin a global worker block was parsed then silently ignored, so it is now rejected at parse time asdocs/config.mdalready documents.3. A
Servercould not be registered twiceunregisterServers()only flippedisRegistered, so the worker slices and maps kept the previous run's entries and a secondInit()with the same*Serverfailed withtwo workers in a server cannot have the same filename. Caddy dodges this by building a freshNewServerperStart(), butdocs/library.mdpresentsNewServer+Initas the library pattern with no hint the instance is single-use.Same commit: the
server_<idx>default was written intos.namepermanently, servers were marked registered about a hundred lines beforeinitWorkers()and the thread setup (a request in that window found neither a worker nor a regular thread, soactivateServers()is split out), andWithWorkerMatcher()withoutWithWorkerServerScope()was a silent no-op that still got path-matched viaglobalWorkersByPath, the opposite of what the matcher asked for.4. Logger and prepared env
Worker startup contexts hardcoded
globalLoggereven though the worker's server was at hand, so worker boot messages and worker stdout bypassed the per-php_serverlogger this branch introduces. The module used to appendWithRequestLogger()to each worker's request options and that line is gone, so nothing else covered it.go_register_server_variables()merged the prepared env whenever the server had one, butregisterPreparedEnv()only runs fromgo_update_request_info(), which returns early without a request. For a server-scoped extension worker handling a message the merge copied whatever the thread-local prepared env held from an earlier request.Tests
Added:
toWorkerOptions()keeps provisioned options, aServerre-registered afterShutdown()still serves its worker and keeps its name, and a request matcher without a server scope is rejected.Both modules pass
go vetincluding test files. The runtime tests need CI: on my box (WSL2) per-thread engine bootstrap of an embed ZTS build is pathologically slow and linking needs dev libs I do not have, so everyInit()-based test is unrunnable locally, including on unmodified base commits.Not included
worker.mercureHubis assigned byconfigureMercure()and never read anywhere; removing the dead field touches thenomercurebuild-tag pair, so I left it alone. Say the word and I will fold it in.