Skip to content

feat: unify php_server logic - #2499

Merged
AlliBalliBaba merged 91 commits into
mainfrom
refactor/phpserver
Aug 21, 2026
Merged

feat: unify php_server logic#2499
AlliBalliBaba merged 91 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
@henderkes
henderkes requested a review from dunglas August 11, 2026 17:27
@AlliBalliBaba

Copy link
Copy Markdown
Contributor Author

Looking through global options again, it probably also makes sense to move maxWaitTime and maxRequests to Server. So maybe an extensible api like this would still be preferrable?

server := NewServer(root string, opts... ServerOption )

func WithServerLogger(logger) ServerOption
func WithServerMaxRequests(num) ServerOption
...

@dunglas

dunglas commented Aug 15, 2026

Copy link
Copy Markdown
Member

I agree, it feels like we'll add more options in the future

@henderkes

Copy link
Copy Markdown
Contributor

Let's get this merged first 😆 . There are plenty other features waiting on this too.

@AlliBalliBaba

Copy link
Copy Markdown
Contributor Author

Added the server options just to not have BC breaks. Ready to merge whenever

@AlliBalliBaba

AlliBalliBaba commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Will merge if there's no other blocker @dunglas. There's still some more cleanup to do, doesn't need to all be in this PR though.

@dunglas

dunglas commented Aug 21, 2026

Copy link
Copy Markdown
Member

LGTM!

@AlliBalliBaba
AlliBalliBaba merged commit 54b5122 into main Aug 21, 2026
45 of 48 checks passed
@AlliBalliBaba
AlliBalliBaba deleted the refactor/phpserver branch August 21, 2026 09:37
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request Sep 6, 2026
Background workers run a script in a loop outside the HTTP request
cycle, sharing the PHP runtime with the request threads. Rebuilt on
Server from php#2499: a background worker attaches to a php_server through
WithWorkerServerScope() like any other worker.

Declared with "background" in a worker block (php_server or global) or
WithWorkerBackground() in Go. name is required and exposed as
$_SERVER['FRANKENPHP_WORKER'], match is rejected, num >= 1. The
lifecycle mirrors HTTP workers: re-run on a cooperative exit, restart
with quadratic backoff on a crash, max_consecutive_failures fails Init()
during startup only. drain() runs on shutdown, reboot and handler
transitions so a parked script wakes up instead of waiting out the
force-kill grace period.

The script gets one handle, frankenphp_get_worker_handle(), a stream
that reaches EOF when the worker is drained, meant to carry control
messages later. It is backed by a socket pair, not a pipe: on Windows
PHP's php_select() only waits properly on sockets before 8.5. Streams
do not own the socket (php_sockop_close() would shutdown() it on
Windows), so every call returns a fresh stream and closing one never
affects another; the read timeout is infinite so a blocking read parks
as well as stream_select() does. Both ends are non-inheritable.

A worker counts as ready on its first wait on the handle (select cast
or read), the background analog of frankenphp_handle_request(): Init()
waits for it, ready_workers counts from it, and an exit before it is a
boot failure. The handle's stream ops, copied from the socket ops at
MINIT, report it once per run.

Worker names are scoped like paths: unique within a php_server or
among global workers. The script sees the declared name; metrics and
logs report a scoped worker as "<server name>:<name>", with a numeric
suffix on server names when two blocks resolve to the same one. The
collision-driven renaming in the Caddy module is gone, and
WithWorkerName() resolves within the request's server first.

Supersedes php#2543 and php#2398.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request Sep 6, 2026
Background workers run a script in a loop outside the HTTP request
cycle, sharing the PHP runtime with the request threads. Rebuilt on
Server from php#2499: a background worker attaches to a php_server through
WithWorkerServerScope() like any other worker.

Declared with "background" in a worker block (php_server or global) or
WithWorkerBackground() in Go. name is required and exposed as
$_SERVER['FRANKENPHP_WORKER'], match is rejected, num >= 1. The
lifecycle mirrors HTTP workers: re-run on a cooperative exit, restart
with quadratic backoff on a crash, max_consecutive_failures fails Init()
during startup only. drain() runs on shutdown, reboot and handler
transitions so a parked script wakes up instead of waiting out the
force-kill grace period.

The script gets one handle, frankenphp_get_worker_handle(), a stream
that reaches EOF when the worker is drained, meant to carry control
messages later. It is backed by a socket pair, not a pipe: on Windows
PHP's php_select() only waits properly on sockets before 8.5. Streams
do not own the socket (php_sockop_close() would shutdown() it on
Windows), so every call returns a fresh stream and closing one never
affects another; the read timeout is infinite so a blocking read parks
as well as stream_select() does. Both ends are non-inheritable.

A worker counts as ready on its first wait on the handle (select cast
or read), the background analog of frankenphp_handle_request(): Init()
waits for it, ready_workers counts from it, and an exit before it is a
boot failure. The handle's stream ops, copied from the socket ops at
MINIT, report it once per run.

Worker names are scoped like paths: unique within a php_server or
among global workers. The script sees the declared name; metrics and
logs report a scoped worker as "<server name>:<name>", with a numeric
suffix on server names when two blocks resolve to the same one. The
collision-driven renaming in the Caddy module is gone, and
WithWorkerName() resolves within the request's server first.

Supersedes php#2543 and php#2398.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request Sep 7, 2026
Background workers run a script in a loop outside the HTTP request
cycle, sharing the PHP runtime with the request threads. Rebuilt on
Server from php#2499: a background worker attaches to a php_server through
WithWorkerServerScope() like any other worker.

Declared with "background" in a worker block (php_server or global) or
WithWorkerBackground() in Go. name is required, match is rejected,
num >= 1. The lifecycle mirrors HTTP workers: re-run on a cooperative
exit, restart with quadratic backoff on a crash, max_consecutive_failures
fails Init() during startup only. drain() runs on shutdown, reboot and
handler transitions so a parked script wakes up instead of waiting out
the force-kill grace period.

Every worker sees its declared name in $_SERVER['FRANKENPHP_WORKER'],
HTTP workers included: the documented contract is to test its presence,
not its value. Background workers also get
$_SERVER['FRANKENPHP_WORKER_BACKGROUND'], so a script serving both roles
can tell them apart with isset().

The script gets one handle, frankenphp_get_worker_handle(), a stream
that reaches EOF when the worker is drained, meant to carry control
messages later. It is backed by a socket pair, not a pipe: on Windows
PHP's php_select() only waits properly on sockets before 8.5. Streams
do not own the socket (php_sockop_close() would shutdown() it on
Windows), so every call returns a fresh stream and closing one never
affects another; the read timeout is infinite so a blocking read parks
as well as stream_select() does. Both ends are non-inheritable.

A worker counts as ready on its first wait on the handle (select cast
or read), the background analog of frankenphp_handle_request(): Init()
waits for it, ready_workers counts from it, and an exit before it is a
boot failure. The handle's stream ops, copied from the socket ops at
MINIT, report it once per run.

Worker names are scoped like paths: unique within a php_server or
among global workers. The script sees the declared name; metrics and
logs report a scoped worker as "<server name>:<name>", with a numeric
suffix on server names when two blocks resolve to the same one. The
collision-driven renaming in the Caddy module is gone, and
WithWorkerName() resolves within the request's server first.

Supersedes php#2543 and php#2398.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request Sep 7, 2026
Background workers run a script in a loop outside the HTTP request
cycle, sharing the PHP runtime with the request threads. Rebuilt on
Server from php#2499: a background worker attaches to a php_server through
WithWorkerServerScope() like any other worker.

Declared with "background" in a worker block (php_server or global) or
WithWorkerBackground() in Go. name is required, match is rejected,
num >= 1. The lifecycle mirrors HTTP workers: re-run on a cooperative
exit, restart with quadratic backoff on a crash, max_consecutive_failures
fails Init() during startup only. drain() runs on shutdown, reboot and
handler transitions so a parked script wakes up instead of waiting out
the force-kill grace period.

Every worker sees its declared name in $_SERVER['FRANKENPHP_WORKER'],
HTTP workers included: the documented contract is to test its presence,
not its value. Background workers also get
$_SERVER['FRANKENPHP_WORKER_BACKGROUND'], so a script serving both roles
can tell them apart with isset().

The script gets one handle, frankenphp_get_worker_handle(), a stream
that reaches EOF when the worker is drained, meant to carry control
messages later. It is backed by a socket pair, not a pipe: on Windows
PHP's php_select() only waits properly on sockets before 8.5. Streams
do not own the socket (php_sockop_close() would shutdown() it on
Windows), so every call returns a fresh stream and closing one never
affects another; the read timeout is infinite so a blocking read parks
as well as stream_select() does. Both ends are non-inheritable.

A worker counts as ready on its first wait on the handle (select cast
or read), the background analog of frankenphp_handle_request(): Init()
waits for it, ready_workers counts from it, and an exit before it is a
boot failure. The handle's stream ops, copied from the socket ops at
MINIT, report it once per run. A run gets one stream: every call returns
the same resource until the script closes it, so fetching the handle in a
loop does not grow the resource list of a request that never ends.

Worker names are scoped like paths: unique within a php_server or
among global workers. The script sees the declared name; metrics and
logs report a scoped worker as "<server name>:<name>", with a numeric
suffix on server names when two blocks resolve to the same one, never a
name another block configured. The collision-driven renaming in the Caddy
module is gone, and WithWorkerName() resolves within the request's server
first. FRANKENPHP_WORKER held "1" in HTTP workers before, and workers of a
php_server block were reported under their bare name unless it collided:
both changes are called out in the docs.

Supersedes php#2543 and php#2398.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request Sep 7, 2026
Background workers run a script in a loop outside the HTTP request
cycle, sharing the PHP runtime with the request threads. Rebuilt on
Server from php#2499: a background worker attaches to a php_server through
WithWorkerServerScope() like any other worker.

Declared with "background" in a worker block (php_server or global) or
WithWorkerBackground() in Go. name is required, match is rejected,
num >= 1. The lifecycle mirrors HTTP workers: re-run on a cooperative
exit, restart with quadratic backoff on a crash, max_consecutive_failures
fails Init() during startup only. drain() runs on shutdown, reboot and
handler transitions so a parked script wakes up instead of waiting out
the force-kill grace period.

Every worker sees its declared name in $_SERVER['FRANKENPHP_WORKER'],
HTTP workers included: the documented contract is to test its presence,
not its value. Background workers also get
$_SERVER['FRANKENPHP_WORKER_BACKGROUND'], so a script serving both roles
can tell them apart with isset().

The script gets one handle, frankenphp_get_worker_handle(), a stream
that reaches EOF when the worker is drained, meant to carry control
messages later. It is backed by a socket pair, not a pipe: on Windows
PHP's php_select() only waits properly on sockets before 8.5. Streams
do not own the socket (php_sockop_close() would shutdown() it on
Windows), so every call returns a fresh stream and closing one never
affects another; the read timeout is infinite so a blocking read parks
as well as stream_select() does. Both ends are non-inheritable.

A worker counts as ready on its first wait on the handle (select cast
or read), the background analog of frankenphp_handle_request(): Init()
waits for it, ready_workers counts from it, and an exit before it is a
boot failure. The handle's stream ops, copied from the socket ops at
MINIT, report it once per run. A run gets one stream: every call returns
the same resource until the script closes it, so fetching the handle in a
loop does not grow the resource list of a request that never ends.

Worker names are scoped like paths: unique within a php_server or
among global workers. The script sees the declared name; metrics and
logs report a scoped worker as "<server name>:<name>", with a numeric
suffix on server names when two blocks resolve to the same one, never a
name another block configured. The collision-driven renaming in the Caddy
module is gone, and WithWorkerName() resolves within the request's server
first. FRANKENPHP_WORKER held "1" in HTTP workers before, and workers of a
php_server block were reported under their bare name unless it collided:
both changes are called out in the docs.

Supersedes php#2543 and php#2398.
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