Skip to content
Permalink
Browse files

saveCache() is atomic, alternative solution [Closes #11]

  • Loading branch information
dg committed Feb 12, 2020
1 parent 01c0c64 commit 6cb7c711320d0f5e8fc40aa4100abd1fdf6b9bbd
Showing with 38 additions and 10 deletions.
  1. +20 −10 src/RobotLoader/RobotLoader.php
  2. +18 −0 tests/Loaders/RobotLoader.stress.phpt
@@ -411,36 +411,46 @@ public function setTempDirectory(string $dir): self
private function loadCache(): void
{
$file = $this->getCacheFile();
$lock = fopen("$file.lock", 'cb+');
if (!$lock || !flock($lock, LOCK_SH)) {
throw new \RuntimeException("Unable to create or acquire shared lock on file '$file.lock'.");
}

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

$handle = fopen("$file.lock", 'cb+');
if (!$handle || !flock($handle, LOCK_EX)) {
if (!flock($lock, LOCK_EX)) {
throw new \RuntimeException("Unable to create or acquire exclusive lock on file '$file.lock'.");
}

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

flock($handle, LOCK_UN);
fclose($handle);
@unlink("$file.lock"); // @ file may become locked on Windows
$this->classes = $this->missing = [];
$this->refreshClasses();
$this->saveCache($lock);
}


/**
* Writes class list to cache.
*/
private function saveCache(): void
private function saveCache($lock = null): void
{
$file = $this->getCacheFile();
$lock = $lock ?: fopen("$file.lock", 'cb+');
if (!$lock || !flock($lock, LOCK_EX)) {
throw new \RuntimeException("Unable to create or acquire exclusive lock on file '$file.lock'.");
}

$code = "<?php\nreturn " . var_export([$this->classes, $this->missing], true) . ";\n";
if (file_put_contents("$file.tmp", $code) !== strlen($code) || !rename("$file.tmp", $file)) {
@unlink("$file.tmp"); // @ - file may not exist
if (file_put_contents($file, $code) !== strlen($code)) {
@unlink($file); // @ - file may not exist
throw new \RuntimeException("Unable to create '$file'.");
}
if (function_exists('opcache_invalidate')) {
@@ -0,0 +1,18 @@
<?php

/**
* @multiple 50
*/

declare(strict_types = 1);

require __DIR__ . '/../../vendor/autoload.php';

$loader = new Nette\Loaders\RobotLoader;
$loader->setAutoRefresh(true);
$loader->setTempDirectory(__DIR__ . '/../tmp');
$loader->addDirectory(__DIR__);
$loader->register();

assert(class_exists(Foo::class) === false);
assert(class_exists(Unknown::class) === false);

4 comments on commit 6cb7c71

@ondrejmirtes

This comment has been minimized.

Copy link

ondrejmirtes replied Feb 12, 2020

Thank you for this! I'm gonna put this in the next PHPStan version tonight :)

@dg

This comment has been minimized.

Copy link
Member Author

dg replied Feb 12, 2020

So let's see if it works.

@ondrejmirtes

This comment has been minimized.

Copy link

ondrejmirtes replied Feb 12, 2020

I guess I shouldn't use the latest WIP commit, but this one :)

@dg

This comment has been minimized.

Copy link
Member Author

dg replied Feb 12, 2020

I put them together. I like the solution with only one file. If it turns out that I was wrong, I'll go back to the two files solution.

Please sign in to comment.
You can’t perform that action at this time.