Skip to content

Commit

Permalink
fixed renaming on windows, when it is read at the same time [Closes #11]
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Feb 28, 2020
1 parent 5301488 commit 726c462
Showing 1 changed file with 28 additions and 10 deletions.
38 changes: 28 additions & 10 deletions src/RobotLoader/RobotLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -411,37 +411,55 @@ public function setTempDirectory(string $dir): self
private function loadCache(): void
{
$file = $this->getCacheFile();

// Solving atomicity to work everywhere is really pain in the ass.
// 1) We want to do as little as possible IO calls on production and also directory and file can be not writable (#19)
// so on Linux we include the file directly without shared lock, therefore, the file must be created atomically by renaming.
// 2) On Windows file cannot be renamed-to while is open (ie by include() #11), so we have to acquire a lock.
$lock = defined('PHP_WINDOWS_VERSION_BUILD')
? $this->acquireLock("$file.lock", LOCK_SH)
: null;

$data = @include $file; // @ file may not exist
if (is_array($data)) {
[$this->classes, $this->missing] = $data;
return;
}

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

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

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


/**
* Writes class list to cache.
*/
private function saveCache(): void
private function saveCache($lock = null): void
{
// we have to acquire a lock to be able safely rename file
// on Linux: that another thread does not rename the same named file earlier
// on Windows: that the file is not read by another thread
$file = $this->getCacheFile();
$tempFile = $file . uniqid('', true) . '.tmp';
$lock = $lock ?: $this->acquireLock("$file.lock", LOCK_EX);
$code = "<?php\nreturn " . var_export([$this->classes, $this->missing], true) . ";\n";
if (file_put_contents($tempFile, $code) !== strlen($code) || !rename($tempFile, $file)) {
@unlink($tempFile); // @ - file may not exist

if (file_put_contents("$file.tmp", $code) !== strlen($code) || !rename("$file.tmp", $file)) {
@unlink("$file.tmp"); // @ file may not exist
throw new \RuntimeException("Unable to create '$file'.");
}
if (function_exists('opcache_invalidate')) {
Expand All @@ -452,7 +470,7 @@ private function saveCache(): void

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

0 comments on commit 726c462

Please sign in to comment.