Bug Report
| Subject |
Details |
| Rector version |
2.6.5 |
On a cold cache, parallel runs intermittently abort with:
[ERROR] Could not process some files, due to:
"Child process error: ".
and the worker JSON on the progress line carries bogus parse errors, varying per run:
{"fatal_errors":["syntax error, unexpected string content \"c343c467a1cdb4d496cd51fa46a79d...\""]}
{"fatal_errors":["Unclosed '(' on line 9"]}
Cause
FileCacheStorage::save() writes a temp file and then copies it over the destination:
FileSystem::write($tmpPath, sprintf("<?php declare(strict_types = 1);\n\nreturn %s;", $exported), null);
$copySuccess = @copy($tmpPath, $filePath);
@unlink($tmpPath);
copy() truncates the destination and streams into it, so it is not atomic. On a cold cache every
analysed file writes a cache entry while the parent process and the workers read those same files; a
reader that lands on a partially copied file loads a truncated PHP file and dies with a parse error
whose message depends on where the truncation falls. The worker's non-zero exit is then surfaced as
Child process error: with empty stderr (ParallelFileProcessor.php:181), which is why no file is
reported and the run fails at 100% with every file already processed.
Reproduction
3183 files, 32-core Linux, PHP 8.5.10, config using
->withCache(cacheDirectory: ..., cacheClass: FileCacheStorage::class):
| Scenario |
Result |
cold cache (rm -rf <cacheDirectory>) before each run |
2/3 runs fail; up to 8/8 under load |
| warm cache, same command |
6/6 pass |
--debug (single process) |
always passes |
withParallel(maxNumberOfProcess: 1), cold cache |
still fails — parent and worker both touch the cache |
Fix
Replacing
$copySuccess = @copy($tmpPath, $filePath);
with
$copySuccess = @\rename($tmpPath, $filePath);
fixes the issue
Bug Report
On a cold cache, parallel runs intermittently abort with:
and the worker JSON on the progress line carries bogus parse errors, varying per run:
Cause
FileCacheStorage::save()writes a temp file and then copies it over the destination:copy()truncates the destination and streams into it, so it is not atomic. On a cold cache everyanalysed file writes a cache entry while the parent process and the workers read those same files; a
reader that lands on a partially copied file loads a truncated PHP file and dies with a parse error
whose message depends on where the truncation falls. The worker's non-zero exit is then surfaced as
Child process error:with empty stderr (ParallelFileProcessor.php:181), which is why no file isreported and the run fails at 100% with every file already processed.
Reproduction
3183 files, 32-core Linux, PHP 8.5.10, config using
->withCache(cacheDirectory: ..., cacheClass: FileCacheStorage::class):rm -rf <cacheDirectory>) before each run--debug(single process)withParallel(maxNumberOfProcess: 1), cold cacheFix
Replacing
with
fixes the issue