Skip to content

feat: frankenphp_get_vars() and WorkerHandle::setVars() - #2635

Open
nicolas-grekas wants to merge 12 commits into
php:mainfrom
nicolas-grekas:bgworker-vars
Open

nicolas-grekas wants to merge 12 commits into
php:mainfrom
nicolas-grekas:bgworker-vars

Conversation

@nicolas-grekas

@nicolas-grekas nicolas-grekas commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #2617: review the commits on top of it.

Shared state for background workers, the vars half of #2287. A worker declares the namespaces it publishes, and readers name the namespace, never the worker:

worker {
	file config.php
	background
	vars redis feature_flags
}
// the background worker, before its first tick()
$handle->setVars('redis', ['dsn' => 'redis://cache:6379']);

// anywhere
$redis = frankenphp_get_vars('redis');
  1. A namespace has one publisher per scope, the reader's php_server first, then the global workers, and Validate() refuses conflicts; a worker publishing vars runs a single thread.
  2. Every declared namespace is published before the first tick(), which the server start waits for, so requests never wait for a snapshot.
  3. Snapshots belong to the worker, not to a run: a restarting worker keeps serving its last ones. They own their memory, so a reboot of the engine can't leave them dangling.

Left out on purpose: the per-request cache and the unchanged-data skip of #2287, until there are numbers for them.

Names were scoped per server so a script could address a background worker
by name within its php_server. The namespaces of php#2635 and php#2636 do that
now and no PHP API takes a worker name, so naming goes back to what main
does: Caddy makes generated names unique, and the server label and the
ServerMetrics interface are gone. The extension worker fixes leave too,
main has the same bugs and they get a pull request of their own.
Since php#2660 worker threads come on top of num_threads and max_threads
bounds the whole process. Background threads were the one exception, which
took a reservation through calculateMaxThreads(), initPHPThreads() and the
automatic max_threads. They now follow the same rule as HTTP workers.
HTTP workers wait for their ready point without a bound too, on builds
without Zend max execution timers, so the bound belongs with both kinds
of worker. Background workers keep the bound of max_execution_time where
those timers exist, and the warning logged after 10 seconds elsewhere.
Init() waits for a background worker to reach its ready point, its first
WorkerHandle::tick(). On a build with Zend max execution timers,
max_execution_time ends a bootstrap that overstays; without them
FrankenPHP disables that limit, so a script that parks before ticking, or
one whose wake-up at start is lost, keeps the server start waiting for
ever with a warning as the only trace.

boot_timeout bounds that wait, 30 seconds by default, the same figure
PHP's own max_execution_time uses for the bootstrap it does bound. The
worker is then drained and stopped through the boot-failure path it
already has, and Init() returns the name of the worker that never ticked.
Zero waits for ever.
@nicolas-grekas
nicolas-grekas force-pushed the bgworker-vars branch 2 times, most recently from aac930d to 445f8bc Compare October 27, 2026 16:21
The timeout reported through a blocking send, so a second timeout could
wait for room in startupFailChan for good, Init() with it. It now reports
like the handlers do and moves the thread to ShuttingDown, so no run starts
after the one that overstayed, whether the timeout lands in a run or
between two failed boots.

The 30s default now only applies where Zend max execution timers are
missing, as the docs said: elsewhere max_execution_time bounds the
bootstrap, and a set_time_limit() raising it is no longer cut short.
Comments and docs that drifted are updated, and the thread budget and the
Caddy directive get the tests they lacked.
frankenphp_get_vars() copied the snapshot inside a Go callback with its
slot read-locked, so a bailout during the copy, past memory_limit or in an
autoloader an enum triggers, left the lock held: the publisher and the next
readers waited for good, Shutdown() with them. Snapshots are now counted
references, the copy runs in C with no lock held and lets go of the
snapshot on every path.

The class name of an enum went to the autoloaders as the persistent string
of the snapshot, so one keeping it pointed into memory the next
publication freed, and readers raced on its refcount. The lookup now uses a
request copy.

A worker waiting on another one at boot now gives up when the start fails,
so an HTTP worker reading vars in its bootstrap no longer keeps Init()
waiting on a publisher that never comes up, and a Caddy JSON config no
longer drops vars declared on an HTTP worker.

This branch has not been deployed

No deployments
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