|
9 | 9 |
|
10 | 10 | use Nette; |
11 | 11 | use Nette\Utils\FileSystem; |
| 12 | +use Nette\Utils\Helpers; |
12 | 13 | use SplFileInfo; |
13 | | -use function array_merge, defined, extension_loaded, file_get_contents, file_put_contents, filemtime, flock, fopen, function_exists, hash, is_array, is_dir, is_file, realpath, rename, serialize, spl_autoload_register, sprintf, strlen, unlink, var_export; |
| 14 | +use function array_merge, extension_loaded, file_get_contents, file_put_contents, filemtime, flock, fopen, function_exists, hash, is_array, is_dir, is_file, realpath, rename, serialize, spl_autoload_register, sprintf, strlen, unlink, usleep, var_export; |
14 | 15 |
|
15 | 16 |
|
16 | 17 | /** |
@@ -439,7 +440,7 @@ private function loadCache(): void |
439 | 440 | // 1) We want to do as little as possible IO calls on production and also directory and file can be not writable (#19) |
440 | 441 | // so on Linux we include the file directly without shared lock, therefore, the file must be created atomically by renaming. |
441 | 442 | // 2) On Windows file cannot be renamed-to while is open (ie by include() #11), so we have to acquire a lock. |
442 | | - $lock = defined('PHP_WINDOWS_VERSION_BUILD') |
| 443 | + $lock = Helpers::IsWindows |
443 | 444 | ? $this->acquireLock("$file.lock", LOCK_SH) |
444 | 445 | : null; |
445 | 446 |
|
@@ -482,14 +483,36 @@ private function saveCache($lock = null): void |
482 | 483 | $file = $this->generateCacheFileName(); |
483 | 484 | $lock = $lock ?: $this->acquireLock("$file.lock", LOCK_EX); |
484 | 485 | $code = "<?php\nreturn " . var_export([$this->classes, $this->missingClasses, $this->emptyFiles], return: true) . ";\n"; |
| 486 | + $this->atomicWrite($file, $code); |
| 487 | + } |
| 488 | + |
485 | 489 |
|
486 | | - if (file_put_contents("$file.tmp", $code) !== strlen($code) || !rename("$file.tmp", $file)) { |
487 | | - @unlink("$file.tmp"); // @ file may not exist |
488 | | - throw new \RuntimeException(sprintf("Unable to create '%s'.", $file)); |
| 490 | + /** |
| 491 | + * Atomically writes $content to $file via a temporary file and rename(). |
| 492 | + * On Windows rename() over a momentarily locked target intermittently fails, so it is retried briefly. |
| 493 | + */ |
| 494 | + private function atomicWrite(string $file, string $content): void |
| 495 | + { |
| 496 | + $tmp = "$file.tmp"; |
| 497 | + if (file_put_contents($tmp, $content) !== strlen($content)) { |
| 498 | + @unlink($tmp); // @ - file may not exist |
| 499 | + throw new \RuntimeException(sprintf("Unable to create '%s'. %s", $file, Helpers::getLastError())); |
| 500 | + } |
| 501 | + |
| 502 | + if (function_exists('opcache_invalidate')) { |
| 503 | + @opcache_invalidate($file, force: true); // @ - can be restricted |
| 504 | + } |
| 505 | + |
| 506 | + for ($attempt = 1; !@rename($tmp, $file); $attempt++) { |
| 507 | + if ($attempt >= 3 || !Helpers::IsWindows) { |
| 508 | + @unlink($tmp); |
| 509 | + throw new \RuntimeException(sprintf("Unable to create '%s'. %s", $file, Helpers::getLastError())); |
| 510 | + } |
| 511 | + usleep(100_000); |
489 | 512 | } |
490 | 513 |
|
491 | 514 | if (function_exists('opcache_invalidate')) { |
492 | | - @opcache_invalidate($file, force: true); // @ can be restricted |
| 515 | + @opcache_invalidate($file, force: true); // @ - can be restricted |
493 | 516 | } |
494 | 517 | } |
495 | 518 |
|
|
0 commit comments