[Bug]: type-coverage cache lock bails out after 100ms and then writes unlocked, corrupting .temp/v3.php
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 72/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- php
- Domain
- testing-qa
Research direction
Start in src/Support/Cache.php and inspect withinLock(), then run the type-coverage command from the report under concurrent workers to reproduce the cache corruption. Trace the write guarded by the lock and verify that contention no longer produces an invalid .temp/v3.php file or a parse error.
Written by the indexing model from the issue text.
Description
What happened
pest --type-coverage fails intermittently in CI with a parse error inside the plugin's own cache file:
PHP Parse error: syntax error, unexpected single-quoted string " => "
in vendor/pestphp/pest-plugin-type-coverage/.temp/v3.php on line 31
Script @php -d memory_limit=512M vendor/bin/pest --type-coverage --min=98 returned with error code 255
The generated .temp/v3.php contains one writer's complete output immediately followed by another's partial output:
28▕ ),
29▕ );e160161e88d10348' =>
30▕ array (
31▕ 0 => '/home/runner/work/.../app/Mail/TaskCommentMail.php',
Line 29 is a finished var_export ();) with the tail of a different cache key appended to it. The next include $filePath then dies.
Root cause
src/Support/Cache.php — withinLock() gives up on the lock and performs the write anyway:
$attempts = 0;
while (! flock($lock, LOCK_EX | LOCK_NB) && $attempts < 100) {
usleep(1000);
$attempts++;
}
if ($attempts >= 100) {
fclose($lock);
return $callback(); // <- runs the write with no lock held
}
So after ~100ms of contention the mutual exclusion is abandoned rather than enforced. The write it guards is:
$content = '<?php return '.var_export($cache, true).';';
if (file_put_contents($filePath, $content) !== false) {
file_put_contents without LOCK_EX truncates then writes non-atomically, so two callers past the bail-out interleave and produce exactly the corruption above.
Contention is high by design: src/Analyser.php fans the file list out through Pokio's async(), and Pokio\Environment::maxProcesses() sizes the pool at cores × 3. On a 12-core machine that is 36 workers, each writing the same file as it finishes:
foreach ($chunks as $files) {
$promises[] = async(function () use ($cache, $files, $testCase, $onProcessedFile) {
Why it only shows up in CI
It is a timing race. A developer machine acquires the lock inside the 100ms budget; a loaded shared CI runner does not, so the bail-out fires. The same commit passes locally (Total: 99.0 %, clean) and fails on a 2-core GitHub Actions runner.
Suggested fix
Either would remove the corruption:
- Don't write unlocked. On exhausting the attempts, skip the cache update (or fail loudly) rather than proceeding — a missed cache entry is recoverable, a corrupt cache file is not.
- Add
LOCK_EXto the write:file_put_contents($filePath, $content, LOCK_EX), so concurrent writers serialise even when the advisory lock was skipped.
Writing to a temp file and rename()ing over the target would also make the swap atomic.
Workaround
Pinning Pokio to a single worker removes the contention. maxProcesses = min(cores × 3, totalMemory / FORK_MEM_PER_PROC) clamped to >= 1, so a large per-process figure forces one:
- name: Type coverage
env:
FORK_MEM_PER_PROC: '999999999999'
run: vendor/bin/pest --type-coverage --min=98
Verified: 36 workers by default, 1 with the variable set. Worth noting there was no measurable cost on our suite — 21s serial from a cold cache versus 39s parallel, so fork overhead exceeded the parallelism gain.
Versions
| Package | Version |
|---|---|
| pestphp/pest | v4.7.5 |
| pestphp/pest-plugin-type-coverage | v4.0.4 |
| nunomaduro/pokio | v1.0.1 |
| PHP | 8.3.17 |
Filed here as issues are disabled on pestphp/pest-plugin-type-coverage.
- Dominant language
- PHP
- Stars
- 11.7k
- Forks
- 538
- Avg merge
- 4d 11h
- Merged PRs (30d)
- 8
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from pestphp/pest
-
[Bug]: [mutate] Windows: @pest-mutate-ignore comments land on line 1 in files with LF line endings Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
getgrav/grav-plugin-api#45 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
RSS-Bridge/rss-bridge#5098 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
phingofficial/phing#2025 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
silverstripe/developer-docs#911 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100