Skip to content

Run bootstrapFiles per forked worker instead of inheriting the parent's - #6264

Merged
ondrejmirtes merged 1 commit into
2.2.xfrom
bootstrap-after-fork
Aug 26, 2026
Merged

Run bootstrapFiles per forked worker instead of inheriting the parent's#6264
ondrejmirtes merged 1 commit into
2.2.xfrom
bootstrap-after-fork

Conversation

@ondrejmirtes

@ondrejmirtes ondrejmirtes commented Aug 25, 2026

Copy link
Copy Markdown
Member

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's objectManagerLoader connects 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:

  • the main thread right before an in-process analysis (AnalyserRunner, at the parallel/in-process routing decision — also covering its fall-throughs: no main script, zero-process schedule);
  • every worker, spawned or forked (WorkerRunnerWorkerCommand defers too, so both worker kinds bootstrap identically in their own process);
  • the main thread after the workers of a parallel analysis (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

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
ondrejmirtes merged commit 55d9d9b into 2.2.x Aug 26, 2026
747 of 763 checks passed
@ondrejmirtes
ondrejmirtes deleted the bootstrap-after-fork branch August 26, 2026 08:58
@staabm

staabm commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Maybe its worth adding private-packagist to our CI pipeline

@ondrejmirtes

Copy link
Copy Markdown
Member Author

It's in there, but it's 🔮 private 🔮

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