From ed5025c10bed232809ce37bf29a4662465c0e3c4 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Sat, 24 Oct 2026 11:33:10 +0200 Subject: [PATCH] Do not delete a container that another process has not journaled yet PHPStan processes that share a tmpDir journal the containers they load, and each one deletes the container files that are not in the journal. A process writes a new container before it journals it, so a concurrent process could delete the file before it was included. The process then failed with "Unable to include ... Container_*.php". A file that is not in the journal is now deleted only when it is more than a week old. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/DependencyInjection/Configurator.php | 8 ++- .../ConfiguratorJournalTest.php | 60 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/DependencyInjection/ConfiguratorJournalTest.php diff --git a/src/DependencyInjection/Configurator.php b/src/DependencyInjection/Configurator.php index 49cc9f546df..5341f38ed25 100644 --- a/src/DependencyInjection/Configurator.php +++ b/src/DependencyInjection/Configurator.php @@ -145,6 +145,7 @@ private function journal(string $currentContainerClassName): void $linesToWrite = []; $usedInTheLastWeek = []; $now = time(); + $week = 3600 * 24 * 7; $currentAlreadyInTheJournal = false; foreach ($journalLines as $journalLine) { if ($journalLine === '') { @@ -157,8 +158,6 @@ private function journal(string $currentContainerClassName): void $className = $journalLineParts[0]; $containerLastUsedTime = (int) $journalLineParts[1]; - $week = 3600 * 24 * 7; - if ($containerLastUsedTime + $week < $now) { continue; } @@ -200,6 +199,11 @@ private function journal(string $currentContainerClassName): void if (in_array($fileClassName, $usedInTheLastWeek, true)) { continue; } + // another PHPStan process sharing this directory writes a new container before it is + // journaled, so a file missing from the journal is deleted only once it is a week old + if ($fileInfo->getMTime() + $week >= $now) { + continue; + } $basePathname = $fileInfo->getPathname(); @unlink($basePathname); @unlink($basePathname . '.lock'); diff --git a/tests/PHPStan/DependencyInjection/ConfiguratorJournalTest.php b/tests/PHPStan/DependencyInjection/ConfiguratorJournalTest.php new file mode 100644 index 00000000000..d7e5465c552 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/ConfiguratorJournalTest.php @@ -0,0 +1,60 @@ +tempDirectory = sys_get_temp_dir() . '/' . uniqid('phpstan-configurator-journal-', true); + } + + #[Override] + protected function tearDown(): void + { + FileSystem::delete($this->tempDirectory); + } + + public function testKeepsAContainerThatAnotherProcessHasNotJournaledYet(): void + { + $configurator = new Configurator(new LoaderFactory(new FileHelper(__DIR__), __DIR__, __DIR__, null, []), true); + $configurator->setTempDirectory($this->tempDirectory); + $directory = $configurator->getContainerCacheDirectory(); + FileSystem::createDir($directory); + + $now = time(); + $eightDaysAgo = $now - 3600 * 24 * 8; + file_put_contents($directory . '/container.journal', sprintf("Container_used:%d\nContainer_expired:%d\n", $now, $eightDaysAgo)); + foreach (['Container_used', 'Container_expired', 'Container_current', 'Container_unjournaled'] as $className) { + file_put_contents($directory . '/' . $className . '.php', ' $this->journal('Container_current'))->call($configurator); + + $this->assertFileExists($directory . '/Container_used.php'); + $this->assertFileExists($directory . '/Container_current.php'); + $this->assertFileExists($directory . '/Container_fresh.php'); + $this->assertFalse(is_file($directory . '/Container_expired.php')); + $this->assertFalse(is_file($directory . '/Container_unjournaled.php')); + } + +}