FileCacheStorage uses copy() instead of rename(), corrupting cache files on parallel cold-cache runs

Open Beginner friendly
#9,876 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
php

Research direction

Start with FileCacheStorage::save(), especially the temporary-file write and destination update, then inspect ParallelFileProcessor.php:181 to understand the reported worker failure. Reproduce with a removed cache directory and parallel processing, and verify that replacing the non-atomic cache update prevents truncated PHP files and the resulting parse errors.

Written by the indexing model from the issue text.

Description

bug

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"]}
Image

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

Dominant language
PHP
Stars
10.4k
Forks
742
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from rectorphp/rector

All issues in rectorphp/rector

Similar issues

More PHP issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.