Run bootstrapFiles per forked worker instead of inheriting the parent's - #6264
Merged
Conversation
ondrejmirtes
force-pushed
the
bootstrap-after-fork
branch
2 times, most recently
from
August 26, 2026 08:27
55c0791 to
483b39d
Compare
A bootstrap file that opens a connection - private-packagist's phpstan-dba bootstrap boots the Symfony kernel and pins the entity manager's PDO, phpstan-doctrine's objectManagerLoader connects the same way - used to run once in the parent, and every forked worker inherited the one open socket. Workers then raced the postgres protocol on it (phpstan-dba executes the analysed SQL inside transactions), crashing the analysis with 'Failed to start transaction' and driver exceptions surfacing string SQLSTATE codes. BootstrapFilesRunner executes the files at most once per process. When the analysis will fork (ForkParallelChecker), CommandHelper::begin() defers them: each forked worker runs them right after the fork (WorkerRunner) - the same per-process semantics a spawned worker always had from its own boot - and the parent runs them once the analysis phase is over and no more forks can happen (AnalyseApplication), before the phases that may reflect analysed code (stub validation, collector rules). A single-threaded run (--debug, missing proc_open()) runs them before analysing in-process; spawned workers and every other command keep the eager begin()-time execution. Autoloaders the files register merge into the source-locator globals at the deferred point - the locators read them lazily. A bootstrap failure in a forked child exits the worker with the error already printed to its collected stdout instead of unwinding past the fork point. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
ondrejmirtes
force-pushed
the
bootstrap-after-fork
branch
from
August 26, 2026 08:46
483b39d to
7f978ac
Compare
Contributor
|
Maybe its worth adding private-packagist to our CI pipeline |
Member
Author
|
It's in there, but it's 🔮 private 🔮 |
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.
Fixes the private-packagist crash after fork-parallel arrived (
Internal error: Failed to start transaction, driver exceptions surfacing string SQLSTATE codes).Mechanism, verified from the CI stacks and the project's config: their phpstan-dba bootstrap file boots the Symfony kernel and pins the entity manager's live PDO (
$em->getConnection()->getNativeConnection()); phpstan-doctrine'sobjectManagerLoaderconnects the same way. bootstrapFiles ran once in the parent, so every forked worker inherited the one open postgres socket — and phpstan-dba executes the analysed SQL inside transactions during analysis, so N workers raced the wire protocol on a single shared fd. Spawned workers never had the problem because each re-ran the bootstrap in its own process.Fix: the analyse flow defers bootstrapFiles and runs them at exactly the places that need them — every
BootstrapFilesRunner::run()call site is a place where the files are known to be needed:AnalyserRunner, at the parallel/in-process routing decision — also covering its fall-throughs: no main script, zero-process schedule);WorkerRunner—WorkerCommanddefers too, so both worker kinds bootstrap identically in their own process);AnalyserRunner, once the loop is over and no more workers fork), and after a fully cached run (AnalyseApplication's zero-files path) — before the phases that may reflect analysed code (stub validation, collector rules).Every other command keeps the eager
begin()-time execution. The once-per-process latch remains for one documented case: a worker forked from a parent that already ran the files (the fixer flow runs them eagerly before its repeated analysis rounds) inherits that execution.Autoloaders the files register merge into the source-locator globals at the deferred point — the locators read them lazily. A bootstrap failure in a forked child exits the worker with the error already printed to its collected stdout instead of unwinding past the fork point.
Verified with a bootstrap file that logs
getmypid()and defines a constant the analysed code uses: fork = 6 worker PIDs + the parent after them; spawn = 6 worker PIDs + the parent after them;--debug= the parent only, before analysis; fully cached rerun = the parent only. All analyse clean; full test suite,make phpstan(baseline untouched), and CS green.🤖 Generated with Claude Code
https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8