Skip to content

Give spawned workers their PHP configuration on the command line - #6328

Open
ondrejmirtes wants to merge 2 commits into
2.2.xfrom
spawned-worker-opcache
Open

Give spawned workers their PHP configuration on the command line#6328
ondrejmirtes wants to merge 2 commits into
2.2.xfrom
spawned-worker-opcache

Conversation

@ondrejmirtes

@ondrejmirtes ondrejmirtes commented Sep 1, 2026

Copy link
Copy Markdown
Member

Follow-up to the OPcache restart series (#6327 and the commits before it).

bin/phpstan runs the restart prologue in every process, spawned workers included, and they never received the phpstan.restarted marker:

  • on pcntl hosts each spawned worker re-exec'd itself once to activate OPcache, rebuilding its command line without the spawn's -d sys_temp_dir and -d extension= (verified by parking a worker and reading its argv);
  • on Windows, where no restart is possible, workers ran without OPcache — and so without the trusted-types pass, which lives in the optimizer.

Commit 1: ProcessHelper now passes spawned workers the restart's OPcache entries plus both markers (phpstan.restarted, phpstan.turboExtensionPath). On Windows every worker additionally gets its own opcache.cache_id: OPcache there is a named file mapping shared by every process of the same user, build and SAPI (ext/opcache/shared_alloc_win32.c), so identically configured workers would all attach to one segment — the concurrently populated shared cache b293f3c took forked workers off, and a reattach that fails is exit(-2) unless a file cache is configured (we blank it). A per-worker id gives each a private segment, the model spawned workers already have on POSIX.

Verification hooks: workers report in their hello whether turbo, OPcache and trusted types took effect; -vvv prints one line per spawned worker (Spawned worker 1/2 checked in: turbo on, OPcache on, trusted types on), and diagnose lists the worker -d entries under the spawn mechanism. The phar-run job now asserts on that report for every PHP version, and the new phar-run-windows job runs the phar on Windows on 8.2 (OPcache entries alone, no binary below 8.3) and 8.5 (with the turbo DLL placed next to the phar).

Commit 2 fixes what the first one exposed in the extension test suites on PHP 7.4/8.0 (Internal error: Class 'PHPStan\TrinaryLogic' not found): AutoloadSourceLocator's trap serves an empty script for the file an autoloader includes, OPcache caches it under the raw include path, and the opcache_invalidate() that is meant to undo that fails on PHP < 8.1 for any non-file:// path (php_resolve_path() refuses such URLs; 8.1 falls back to the given name). PHPStan's own classes that preload.php leaves to the autoloader — the turbo-shadowed ones — are autoloaded from the phar as phar://…/vendor/composer/../../src/…, so the first cold use after the trap died. Where the invalidation cannot work, the trap now serves a parse error instead (compiles to nothing, so nothing is cached) and the locator catches it. Reproduced and verified locally on PHP 7.4 with the CI phar (phpstan-nette, spawned worker: OPcache on[OK] No errors).

🤖 Generated with Claude Code

https://claude.ai/code/session_01VdGL6QPGhAzYmN2zU4gwhH

persist-credentials: false

- name: "Install PHP"
uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2
ProcessHelper now passes spawned workers the OPcache entries of the
TurboProcessRestarter restart, the restart marker, and the extension
path marker next to the extension itself. Until now a spawned worker
ran bin/phpstan's restart prologue unmarked: on a pcntl host it
re-executed itself once to activate OPcache - rebuilding its command
line without the sys_temp_dir and extension entries of the spawn - and
on Windows, where no restart is possible, it ran without OPcache and so
without the turbo trusted-types pass, which lives in the optimizer.

Windows OPcache is a named file mapping shared by every process of the
same user, build and SAPI, so identically configured workers would all
attach to one segment - the concurrently populated shared cache forked
workers were taken off in b293f3c, and a reattach that fails is a
fatal startup error. Each Windows worker gets its own opcache.cache_id
instead: a private segment, the model spawned workers have elsewhere.

Workers now report in their hello whether turbo, OPcache and trusted
types took effect; ParallelAnalyser prints it per spawned worker at
-vvv, and diagnose lists the worker -d entries under the spawn
mechanism. The phar-run job asserts on the report for every PHP
version, and the new phar-run-windows job runs the phar on Windows with
and without the turbo DLL next to it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdGL6QPGhAzYmN2zU4gwhH
@ondrejmirtes
ondrejmirtes force-pushed the spawned-worker-opcache branch from 14ff3d2 to e1eb1e5 Compare September 1, 2026 14:51
…ts empty script

AutoloadSourceLocator locates a class not yet loaded by running the
autoloaders under FileReadTrapStreamWrapper, which records the file an
autoloader includes and serves an empty script in its place. With
OPcache active that empty script is compiled and cached under the path
the include was given; the locator invalidates the trapped files
afterwards, but on PHP < 8.1 opcache_invalidate() fails for the path of
any stream wrapper other than file:// - zend_accel_invalidate() resolves
the path first and php_resolve_path() refuses such URLs (8.1 falls back
to the name as given). The poisoned entry then answers every later real
include of the same path, the class is never declared, and its first
cold use dies with "Class not found".

PHPStan's own classes that preload.php leaves to the autoloader (the
turbo-shadowed ones, TrinaryLogic among them) are exactly such files when
running from the phar - autoloaded as
phar://.../vendor/composer/../../src/TrinaryLogic.php by the phar's
Composer classmap - and analysing code that references them triggers the
trap. Spawned workers on PHP 7.4 and 8.0 started running with OPcache in
the previous commit, which is how the extension test suites hit it.

Where the invalidation cannot work, the trap now serves a parse error
instead: it compiles to nothing, so OPcache stores nothing, and the
locator catches the ParseError - the file was recorded before the
include compiled it. Everywhere else the empty script stays, so
autoloaders that require several files in a row keep being captured in
full.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdGL6QPGhAzYmN2zU4gwhH
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.

2 participants