Skip to content

Commit 00891e5

Browse files
committed
ContainerLoader: make atomic write robust against transient Windows file locks
On Windows, rename() over an existing cached container file intermittently fails with "Access is denied" when the target is momentarily locked by another process (antivirus scanning the freshly written file, or a memory-mapped opcache handle). Unlike POSIX, the replace is not atomic against open handles. Extracts the tmp-write + rename into atomicWrite(), which now drops the target's opcache handle before renaming and, on Windows only, retries the rename a few times with a short backoff. On other platforms behavior is unchanged — a single rename failure still throws immediately. Fixes flaky ContainerLoader.hotReload.
1 parent ec9a161 commit 00891e5

2 files changed

Lines changed: 37 additions & 8 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"nette/php-generator": "^4.1.6",
2323
"nette/robot-loader": "^4.0",
2424
"nette/schema": "^1.2.5",
25-
"nette/utils": "^4.0"
25+
"nette/utils": "^4.0.6"
2626
},
2727
"require-dev": {
2828
"nette/tester": "^2.6",

src/DI/ContainerLoader.php

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
namespace Nette\DI;
99

1010
use Nette;
11-
use function class_exists, file_get_contents, file_put_contents, flock, fopen, function_exists, hash, is_file, rename, serialize, sprintf, strlen, substr, unlink, unserialize;
11+
use function class_exists, file_get_contents, file_put_contents, flock, fopen, function_exists, hash, is_file, rename, serialize, sprintf, strlen, substr, unlink, unserialize, usleep;
1212

1313

1414
/**
@@ -74,12 +74,7 @@ private function loadFile(string $class, \Closure $generator): void
7474
}
7575

7676
foreach ($toWrite as $name => $content) {
77-
if (file_put_contents("$name.tmp", $content) !== strlen($content) || !rename("$name.tmp", $name)) {
78-
@unlink("$name.tmp"); // @ - file may not exist
79-
throw new Nette\IOException(sprintf("Unable to create file '%s'.", $name));
80-
} elseif (function_exists('opcache_invalidate')) {
81-
@opcache_invalidate($name, force: true); // @ can be restricted
82-
}
77+
$this->atomicWrite($name, $content);
8378
}
8479
}
8580

@@ -90,6 +85,40 @@ private function loadFile(string $class, \Closure $generator): void
9085
}
9186

9287

88+
/**
89+
* Atomically writes $content to $file through a temporary file and rename().
90+
*
91+
* On Windows the rename intermittently fails with "Access is denied" when the target is
92+
* momentarily locked (antivirus or a memory-mapped opcache handle); unlike POSIX the replace
93+
* is not atomic against open handles. So the opcache handle is dropped first and the rename
94+
* retried briefly. Elsewhere a single failure throws at once.
95+
*/
96+
private function atomicWrite(string $file, string $content): void
97+
{
98+
$tmp = "$file.tmp";
99+
if (file_put_contents($tmp, $content) !== strlen($content)) {
100+
@unlink($tmp); // @ - file may not exist
101+
throw new Nette\IOException(sprintf("Unable to create file '%s'. %s", $file, Nette\Utils\Helpers::getLastError()));
102+
}
103+
104+
if (function_exists('opcache_invalidate')) {
105+
@opcache_invalidate($file, force: true); // @ can be restricted; frees a possible handle on the old target
106+
}
107+
108+
for ($attempt = 1; !@rename($tmp, $file); $attempt++) { // @ is escalated to exception below
109+
if ($attempt >= 3 || !Nette\Utils\Helpers::IsWindows) {
110+
@unlink($tmp); // @ - file may not exist
111+
throw new Nette\IOException(sprintf("Unable to create file '%s'. %s", $file, Nette\Utils\Helpers::getLastError()));
112+
}
113+
usleep(100_000);
114+
}
115+
116+
if (function_exists('opcache_invalidate')) {
117+
@opcache_invalidate($file, force: true); // @ can be restricted; refresh with the new content
118+
}
119+
}
120+
121+
93122
private function isExpired(string $file, ?string &$updatedMeta = null): bool
94123
{
95124
if ($this->autoRebuild) {

0 commit comments

Comments
 (0)