Skip to content

Commit 726c462

Browse files
committed
fixed renaming on windows, when it is read at the same time [Closes #11]
1 parent 5301488 commit 726c462

File tree

1 file changed

+28
-10
lines changed

1 file changed

+28
-10
lines changed

src/RobotLoader/RobotLoader.php

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -411,37 +411,55 @@ public function setTempDirectory(string $dir): self
411411
private function loadCache(): void
412412
{
413413
$file = $this->getCacheFile();
414+
415+
// Solving atomicity to work everywhere is really pain in the ass.
416+
// 1) We want to do as little as possible IO calls on production and also directory and file can be not writable (#19)
417+
// so on Linux we include the file directly without shared lock, therefore, the file must be created atomically by renaming.
418+
// 2) On Windows file cannot be renamed-to while is open (ie by include() #11), so we have to acquire a lock.
419+
$lock = defined('PHP_WINDOWS_VERSION_BUILD')
420+
? $this->acquireLock("$file.lock", LOCK_SH)
421+
: null;
422+
414423
$data = @include $file; // @ file may not exist
415424
if (is_array($data)) {
416425
[$this->classes, $this->missing] = $data;
417426
return;
418427
}
419428

429+
if ($lock) {
430+
flock($lock, LOCK_UN); // release shared lock so we can get exclusive
431+
}
420432
$lock = $this->acquireLock("$file.lock", LOCK_EX);
421433

434+
// while waiting for exclusive lock, someone might have already created the cache
422435
$data = @include $file; // @ file may not exist
423436
if (is_array($data)) {
424437
[$this->classes, $this->missing] = $data;
425-
} else {
426-
$this->rebuild();
438+
return;
427439
}
428440

429-
flock($lock, LOCK_UN);
430-
fclose($lock);
431-
@unlink("$file.lock"); // @ file may become locked on Windows
441+
$this->classes = $this->missing = [];
442+
$this->refreshClasses();
443+
$this->saveCache($lock);
444+
// On Windows concurrent creation and deletion of a file can cause a error 'permission denied',
445+
// therefore, we will not delete the lock file. Windows is peace of shit.
432446
}
433447

434448

435449
/**
436450
* Writes class list to cache.
437451
*/
438-
private function saveCache(): void
452+
private function saveCache($lock = null): void
439453
{
454+
// we have to acquire a lock to be able safely rename file
455+
// on Linux: that another thread does not rename the same named file earlier
456+
// on Windows: that the file is not read by another thread
440457
$file = $this->getCacheFile();
441-
$tempFile = $file . uniqid('', true) . '.tmp';
458+
$lock = $lock ?: $this->acquireLock("$file.lock", LOCK_EX);
442459
$code = "<?php\nreturn " . var_export([$this->classes, $this->missing], true) . ";\n";
443-
if (file_put_contents($tempFile, $code) !== strlen($code) || !rename($tempFile, $file)) {
444-
@unlink($tempFile); // @ - file may not exist
460+
461+
if (file_put_contents("$file.tmp", $code) !== strlen($code) || !rename("$file.tmp", $file)) {
462+
@unlink("$file.tmp"); // @ file may not exist
445463
throw new \RuntimeException("Unable to create '$file'.");
446464
}
447465
if (function_exists('opcache_invalidate')) {
@@ -452,7 +470,7 @@ private function saveCache(): void
452470

453471
private function acquireLock(string $file, int $mode)
454472
{
455-
$handle = @fopen($file, 'cb+'); // @ is escalated to exception
473+
$handle = @fopen($file, 'w'); // @ is escalated to exception
456474
if (!$handle) {
457475
throw new \RuntimeException("Unable to create file '$file'. " . error_get_last()['message']);
458476
} elseif (!@flock($handle, $mode)) { // @ is escalated to exception

0 commit comments

Comments
 (0)