Skip to content

feat: unify php_server logic - #2499

Open
AlliBalliBaba wants to merge 82 commits into
mainfrom
refactor/phpserver
Open

feat: unify php_server logic#2499
AlliBalliBaba wants to merge 82 commits into
mainfrom
refactor/phpserver

Conversation

@AlliBalliBaba

Copy link
Copy Markdown
Contributor

Currently the concept of a php_server only exists on the caddy side and not the FrankenPHP side.
Lately we have been moving more and more in a direction of scoping requests or workers to specific php_server blocks.

This PR is an attempt at refactoring the current php_server logic so it is properly mirrored on the FrankenPHP side without BC breaks for library users (and to prevent future bugs like mentioned in #2487)

Comment thread options.go Outdated
Comment thread options.go Outdated
Comment thread options.go Outdated
Comment thread phpserver.go Outdated
nicolas-grekas and others added 3 commits July 20, 2026 22:54
Follow-up to #2499, targeting `refactor/phpserver`. Docs-only.

The refactor introduces new public API for library users (`NewServer`,
`WithServer`, `Server.ServeHTTP`, `WithWorkerServerScope`,
`WithWorkerMatcher`) with no documentation outside godoc. This adds a
`docs/library.md` page covering:

- minimal setup: `NewServer` + `Init(WithServer(...))` +
`Server.ServeHTTP` as an `http.Handler`
- registering multiple servers (the library equivalent of multiple
`php_server` blocks)
- scoping workers to a server, by path and by request matcher
- per-request options
- the compatibility path: package-level `ServeHTTP()` keeps working
through the fallback server

Also linked from the README docs list and `llms.txt`. The examples
follow the current `NewServer` signature on the branch; if #2529 lands
first, the snippets need a one-line update (happy to rebase either way).
Translations are left to the usual translation update flow.
@AlliBalliBaba

Copy link
Copy Markdown
Contributor Author

This PR just got a bit bigger, mainly due to @nicolas-grekas contributions (#2529 #2530 #2531 #2532).

IMO this PR does not need to contain everything relating to serves yet, probably to come in future PRs:

  • servers in metrics/logging
  • Caching server env
  • scoping of background workers or similar mechanisms
  • scoping of other configurations or pools to servers

@henderkes

Copy link
Copy Markdown
Contributor

In the process of reviewing, generally seems solid, doing a tiny bit of cleanup now.

@henderkes henderkes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: seems like WithWorkerName can reach for cross-server workers by name, perhaps that's worth a note. I don't think it was possible before, because it looked in the modules local list and then fell through to the global one.

@nicolas-grekas

Copy link
Copy Markdown
Contributor

Would anyone be available to merge this one? Or let me know how I can help get it merged? 🙏

@henderkes

Copy link
Copy Markdown
Contributor

One of you two please merge. It's a bigger refactor so we should have more than only one approval :)

nicolas-grekas and others added 4 commits August 5, 2026 22:29
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()` fills `workerConfig.options`, but the new
`toWorkerOptions()` builds a fresh slice and never appends it, where
`Start()` used to pass `w.options` directly. The field is now written
and never read.

`WithWorkerMercureHub()` 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. #2543 extends the same
function and inherits the bug.

### 2. `FrankenPHPModule.server` can be nil in `ServeHTTP()`

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 where `main` returned a clean `ErrNotRunning`.

Two neighbouring lifecycle issues are fixed in the same commit: modules
left in `app.modules` by a config that failed to provision are
registered again by the next reload (the app is a process singleton and
`reset()` only runs at the end of `Start()`), and `match` in a global
worker block was parsed then silently ignored, so it is now rejected at
parse time as `docs/config.md` already documents.

### 3. A `Server` could not be registered twice

`unregisterServers()` only flipped `isRegistered`, so the worker slices
and maps kept the previous run's entries and a second `Init()` with the
same `*Server` failed with `two workers in a server cannot have the same
filename`. Caddy dodges this by building a fresh `NewServer` per
`Start()`, but `docs/library.md` presents `NewServer` + `Init` as the
library pattern with no hint the instance is single-use.

Same commit: the `server_<idx>` default was written into `s.name`
permanently, servers were marked registered about a hundred lines before
`initWorkers()` and the thread setup (a request in that window found
neither a worker nor a regular thread, so `activateServers()` is split
out), and `WithWorkerMatcher()` without `WithWorkerServerScope()` was a
silent no-op that still got path-matched via `globalWorkersByPath`, the
opposite of what the matcher asked for.

### 4. Logger and prepared env

Worker startup contexts hardcoded `globalLogger` even though the
worker's server was at hand, so worker boot 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 copied
whatever the thread-local prepared env held from an earlier request.

### Tests

Added: `toWorkerOptions()` keeps provisioned options, a `Server`
re-registered after `Shutdown()` still serves its worker and keeps its
name, and a request matcher without a server scope is rejected.

Both modules pass `go vet` including 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
every `Init()`-based test is unrunnable locally, including on unmodified
base commits.

### Not included

`worker.mercureHub` is assigned by `configureMercure()` and never read
anywhere; removing the dead field touches the `nomercure` build-tag
pair, so I left it alone. Say the word and I will fold it in.
@AlliBalliBaba

AlliBalliBaba commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Some more small fixes by @nicolas-grekas are now merged in (see #2565)

I also added some tweaks to make startups race-free.

  • ServeHTTP() will now block up to 10s waiting until Init() has finished, making it impossible to send requests before startup. (the http server start can happen before the FrankenPHP start)
  • Shutdown() will now always wait for Startup() to finish, removing any potential race detections (mainly annoying in tests)

Comment thread caddy/app.go
Comment on lines -72 to 82
func (f FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
func (*FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "frankenphp",
New: func() caddy.Module { return &f },
New: func() caddy.Module { return &FrankenPHPApp{} },
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes much more sense to not return the same app instance. Probably was the root cause of many subtle bugs in tests

@AlliBalliBaba

Copy link
Copy Markdown
Contributor Author

I'll probably merge this branch soon if that's fine with everyone involved . Otherwise we'll just end up stalling other PRs

@dunglas

dunglas commented Aug 5, 2026

Copy link
Copy Markdown
Member

I'm trying to tag a release. I hope it will be good tomorrow.

Please wait for the new release to be tagged before merging, so we'll have more time to test this when merged in main.

Comment thread caddy/module.go Outdated
Comment thread context.go Outdated
Comment thread caddy/app.go Outdated
Comment thread caddy/app.go Outdated
Comment thread caddy/app.go Outdated
Comment thread caddy/module.go Outdated
Comment thread caddy/workerconfig.go Outdated
Comment thread cgi.go Outdated
Comment thread frankenphp.go Outdated
Comment thread docs/library.md
@@ -0,0 +1,98 @@
---

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you update ExampleServeHTTP(), which is directly rendered on https://pkg.go.dev, instead of adding a new entry? Or at least couldn't we avoid duplications and redirect from this new page to the Go pkg page?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we don't necessarily need this at all, but I can update the example and link to https://pkg.go.dev/.
If that's fine with @nicolas-grekas

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.

5 participants