diff --git a/src/Driver/BlockingDriver.php b/src/Driver/BlockingDriver.php index 9137d7d..8da7283 100644 --- a/src/Driver/BlockingDriver.php +++ b/src/Driver/BlockingDriver.php @@ -51,29 +51,16 @@ final class BlockingDriver implements Driver public function getStatus(string $path): Promise { - if ($stat = StatCache::get($path)) { - return new Success($stat); - } + \clearstatcache(true, $path); - if ($stat = @\stat($path)) { - StatCache::set($path, $stat); - \clearstatcache(true, $path); - } else { - $stat = null; - } - - return new Success($stat); + return new Success(@\stat($path) ?: null); } public function getLinkStatus(string $path): Promise { - if ($stat = @\lstat($path)) { - \clearstatcache(true, $path); - } else { - $stat = null; - } + \clearstatcache(true, $path); - return new Success($stat); + return new Success(@\lstat($path) ?: null); } public function createSymlink(string $target, string $link): Promise @@ -154,8 +141,6 @@ final class BlockingDriver implements Driver public function deleteFile(string $path): Promise { - StatCache::clear($path); - try { \set_error_handler(static function ($type, $message) use ($path) { throw new FilesystemException("Could not delete file '{$path}': {$message}"); @@ -225,8 +210,6 @@ final class BlockingDriver implements Driver public function deleteDirectory(string $path): Promise { - StatCache::clear($path); - try { \set_error_handler(static function ($type, $message) use ($path) { throw new FilesystemException("Could not remove directory '{$path}': {$message}"); @@ -282,8 +265,6 @@ final class BlockingDriver implements Driver throw new FilesystemException("Failed to change permissions for '{$path}'"); } - StatCache::clear($path); - return new Success; } catch (FilesystemException $e) { return new Failure($e); @@ -307,8 +288,6 @@ final class BlockingDriver implements Driver throw new FilesystemException("Failed to change owner for '{$path}'"); } - StatCache::clear($path); - return new Success; } catch (FilesystemException $e) { return new Failure($e); @@ -331,8 +310,6 @@ final class BlockingDriver implements Driver throw new FilesystemException("Failed to touch '{$path}'"); } - StatCache::clear($path); - return new Success; } catch (FilesystemException $e) { return new Failure($e); diff --git a/src/Driver/EioDriver.php b/src/Driver/EioDriver.php index b5b8bdb..725c889 100644 --- a/src/Driver/EioDriver.php +++ b/src/Driver/EioDriver.php @@ -49,10 +49,6 @@ final class EioDriver implements Driver public function getStatus(string $path): Promise { - if ($stat = StatCache::get($path)) { - return new Success($stat); - } - $deferred = new Deferred; $this->poll->listen($deferred->promise()); @@ -219,7 +215,6 @@ final class EioDriver implements Driver $priority = \EIO_PRI_DEFAULT; \eio_chmod($path, $mode, $priority, [$this, "onGenericResult"], $deferred); - StatCache::clearOn($deferred->promise(), $path); return $deferred->promise(); } @@ -231,7 +226,6 @@ final class EioDriver implements Driver $priority = \EIO_PRI_DEFAULT; \eio_chown($path, $uid ?? -1, $gid ?? -1, $priority, [$this, "onGenericResult"], $deferred); - StatCache::clearOn($deferred->promise(), $path); return $deferred->promise(); } @@ -246,7 +240,6 @@ final class EioDriver implements Driver $priority = \EIO_PRI_DEFAULT; \eio_utime($path, $accessTime, $modificationTime, $priority, [$this, "onGenericResult"], $deferred); - StatCache::clearOn($deferred->promise(), $path); return $deferred->promise(); } @@ -343,7 +336,6 @@ final class EioDriver implements Driver if ($result === -1) { $deferred->fail(new FilesystemException(\eio_get_last_error($req))); } else { - StatCache::set($path, $result); $handle = new EioFile($this->poll, $fh, $path, $mode, $result["size"]); $deferred->resolve($handle); } @@ -355,7 +347,6 @@ final class EioDriver implements Driver if ($result === -1) { $deferred->resolve(null); } else { - StatCache::set($path, $result); $deferred->resolve($result); } } @@ -394,7 +385,6 @@ final class EioDriver implements Driver if ($result === -1) { $deferred->fail(new FilesystemException(\eio_get_last_error($req))); } else { - StatCache::clear($path); $deferred->resolve(); } } @@ -421,7 +411,6 @@ final class EioDriver implements Driver if ($result === -1) { $deferred->fail(new FilesystemException(\eio_get_last_error($req))); } else { - StatCache::clear($path); $deferred->resolve(); } } diff --git a/src/Driver/ParallelDriver.php b/src/Driver/ParallelDriver.php index 227d6b4..45fdbaf 100644 --- a/src/Driver/ParallelDriver.php +++ b/src/Driver/ParallelDriver.php @@ -45,24 +45,12 @@ final class ParallelDriver implements Driver public function deleteFile(string $path): Promise { - $promise = new Coroutine($this->runFileTask(new Internal\FileTask("deleteFile", [$path]))); - StatCache::clearOn($promise, $path); - return $promise; + return new Coroutine($this->runFileTask(new Internal\FileTask("deleteFile", [$path]))); } public function getStatus(string $path): Promise { - if ($stat = StatCache::get($path)) { - return new Success($stat); - } - - return call(function () use ($path) { - $stat = yield from $this->runFileTask(new Internal\FileTask("getStatus", [$path])); - if (!empty($stat)) { - StatCache::set($path, $stat); - } - return $stat; - }); + return new Coroutine($this->runFileTask(new Internal\FileTask("getStatus", [$path]))); } public function move(string $from, string $to): Promise @@ -102,23 +90,17 @@ final class ParallelDriver implements Driver public function deleteDirectory(string $path): Promise { - $promise = new Coroutine($this->runFileTask(new Internal\FileTask("deleteDirectory", [$path]))); - StatCache::clearOn($promise, $path); - return $promise; + return new Coroutine($this->runFileTask(new Internal\FileTask("deleteDirectory", [$path]))); } public function changePermissions(string $path, int $mode): Promise { - $promise = new Coroutine($this->runFileTask(new Internal\FileTask("changePermissions", [$path, $mode]))); - StatCache::clearOn($promise, $path); - return $promise; + return new Coroutine($this->runFileTask(new Internal\FileTask("changePermissions", [$path, $mode]))); } public function changeOwner(string $path, ?int $uid, ?int $gid): Promise { - $promise = new Coroutine($this->runFileTask(new Internal\FileTask("changeOwner", [$path, $uid, $gid]))); - StatCache::clearOn($promise, $path); - return $promise; + return new Coroutine($this->runFileTask(new Internal\FileTask("changeOwner", [$path, $uid, $gid]))); } public function getLinkStatus(string $path): Promise @@ -128,12 +110,10 @@ final class ParallelDriver implements Driver public function touch(string $path, ?int $modificationTime, ?int $accessTime): Promise { - $promise = new Coroutine($this->runFileTask(new Internal\FileTask( + return new Coroutine($this->runFileTask(new Internal\FileTask( "touch", [$path, $modificationTime, $accessTime] ))); - StatCache::clearOn($promise, $path); - return $promise; } public function read(string $path): Promise diff --git a/src/Driver/StatusCachingDriver.php b/src/Driver/StatusCachingDriver.php new file mode 100644 index 0000000..0160bb5 --- /dev/null +++ b/src/Driver/StatusCachingDriver.php @@ -0,0 +1,136 @@ +driver = $driver; + $this->statusCache = new Cache(1000, 1024); + } + + public function openFile(string $path, string $mode): Promise + { + return call(function () use ($path, $mode) { + $file = yield $this->driver->openFile($path, $mode); + + return new StatusCachingFile($file, function () use ($path) { + $this->invalidate([$path], new Success); + }); + }); + } + + public function getStatus(string $path): Promise + { + if ($cachedStat = $this->statusCache->get($path)) { + return new Success($cachedStat); + } + + return $this->driver->getStatus($path); + } + + public function getLinkStatus(string $path): Promise + { + return $this->driver->getLinkStatus($path); + } + + public function createSymlink(string $target, string $link): Promise + { + return $this->invalidate([$target, $link], $this->driver->createSymlink($target, $link)); + } + + public function createHardlink(string $target, string $link): Promise + { + return $this->invalidate([$target, $link], $this->driver->createHardlink($target, $link)); + } + + public function resolveSymlink(string $target): Promise + { + return $this->driver->resolveSymlink($target); + } + + public function move(string $from, string $to): Promise + { + return $this->invalidate([$from, $to], $this->driver->move($from, $to)); + } + + public function deleteFile(string $path): Promise + { + return $this->invalidate([$path], $this->driver->deleteFile($path)); + } + + public function createDirectory(string $path, int $mode = 0777): Promise + { + return $this->invalidate([$path], $this->driver->createDirectory($path, $mode)); + } + + public function createDirectoryRecursively(string $path, int $mode = 0777): Promise + { + return $this->invalidate([$path], $this->driver->createDirectoryRecursively($path, $mode)); + } + + public function deleteDirectory(string $path): Promise + { + return $this->invalidate([$path], $this->driver->deleteDirectory($path)); + } + + public function listFiles(string $path): Promise + { + return $this->driver->listFiles($path); + } + + public function changePermissions(string $path, int $mode): Promise + { + return $this->invalidate([$path], $this->driver->changePermissions($path, $mode)); + } + + public function changeOwner(string $path, ?int $uid, ?int $gid): Promise + { + return $this->invalidate([$path], $this->driver->changeOwner($path, $uid, $gid)); + } + + public function touch(string $path, ?int $modificationTime, ?int $accessTime): Promise + { + return $this->invalidate([$path], $this->driver->touch($path, $modificationTime, $accessTime)); + } + + public function read(string $path): Promise + { + return $this->driver->read($path); + } + + public function write(string $path, string $contents): Promise + { + return $this->invalidate([$path], $this->driver->write($path, $contents)); + } + + private function invalidate(array $paths, Promise $promise): Promise + { + foreach ($paths as $path) { + $this->statusCache->delete($path); + } + + if (!$promise instanceof Success) { + $promise->onResolve(function () use ($paths) { + foreach ($paths as $path) { + $this->statusCache->delete($path); + } + }); + } + + return $promise; + } +} \ No newline at end of file diff --git a/src/Driver/StatusCachingFile.php b/src/Driver/StatusCachingFile.php new file mode 100644 index 0000000..c079e6d --- /dev/null +++ b/src/Driver/StatusCachingFile.php @@ -0,0 +1,81 @@ +file = $file; + $this->invalidateCallback = $invalidateCallback; + } + + public function read(int $length = self::DEFAULT_READ_LENGTH): Promise + { + return $this->file->read($length); + } + + public function write(string $data): Promise + { + return $this->invalidate($this->file->write($data)); + } + + public function end(string $data = ""): Promise + { + return $this->invalidate($this->file->end($data)); + } + + public function close(): Promise + { + return $this->file->close(); + } + + public function seek(int $position, int $whence = self::SEEK_SET): Promise + { + return $this->file->seek($position, $whence); + } + + public function tell(): int + { + return $this->file->tell(); + } + + public function eof(): bool + { + return $this->file->eof(); + } + + public function getPath(): string + { + return $this->file->getPath(); + } + + public function getMode(): string + { + return $this->file->getMode(); + } + + public function truncate(int $size): Promise + { + return $this->invalidate($this->file->truncate($size)); + } + + private function invalidate(Promise $promise): Promise + { + $promise->onResolve(function () { + ($this->invalidateCallback)(); + }); + + return $promise; + } +} \ No newline at end of file diff --git a/src/Driver/UvDriver.php b/src/Driver/UvDriver.php index 698dcd2..4289667 100644 --- a/src/Driver/UvDriver.php +++ b/src/Driver/UvDriver.php @@ -65,10 +65,6 @@ final class UvDriver implements Driver public function getStatus(string $path): Promise { - if ($stat = StatCache::get($path)) { - return new Success($stat); - } - $deferred = new Deferred; $this->poll->listen($deferred->promise()); @@ -86,8 +82,6 @@ final class UvDriver implements Driver unset($stat['link']); } - StatCache::set($path, $stat); - $deferred->resolve($stat); }; @@ -183,7 +177,6 @@ final class UvDriver implements Driver $this->poll->listen($deferred->promise()); \uv_fs_rename($this->loop, $from, $to, $this->createGenericCallback($deferred, "Could not rename file")); - StatCache::clearOn($deferred->promise(), $from); return $deferred->promise(); } @@ -194,7 +187,6 @@ final class UvDriver implements Driver $this->poll->listen($deferred->promise()); \uv_fs_unlink($this->loop, $path, $this->createGenericCallback($deferred, "Could not unlink file")); - StatCache::clearOn($deferred->promise(), $path); return $deferred->promise(); } @@ -260,7 +252,6 @@ final class UvDriver implements Driver $this->poll->listen($deferred->promise()); \uv_fs_rmdir($this->loop, $path, $this->createGenericCallback($deferred, "Could not remove directory")); - StatCache::clearOn($deferred->promise(), $path); return $deferred->promise(); } @@ -303,7 +294,6 @@ final class UvDriver implements Driver $callback = $this->createGenericCallback($deferred, "Could not change file permissions"); \uv_fs_chmod($this->loop, $path, $mode, $callback); - StatCache::clearOn($deferred->promise(), $path); return $deferred->promise(); } @@ -316,7 +306,6 @@ final class UvDriver implements Driver $callback = $this->createGenericCallback($deferred, "Could not change file owner"); \uv_fs_chown($this->loop, $path, $uid ?? -1, $gid ?? -1, $callback); - StatCache::clearOn($deferred->promise(), $path); return $deferred->promise(); } @@ -331,7 +320,6 @@ final class UvDriver implements Driver $callback = $this->createGenericCallback($deferred, "Could not touch file"); \uv_fs_utime($this->loop, $path, $modificationTime, $accessTime, $callback); - StatCache::clearOn($deferred->promise(), $path); return $deferred->promise(); } @@ -398,7 +386,6 @@ final class UvDriver implements Driver } else { \uv_fs_fstat($this->loop, $fh, function ($fh, $stat) use ($openArr): void { if (\is_resource($fh)) { - StatCache::set($openArr[1], $stat); $this->finalizeHandle($fh, $stat["size"], $openArr); } else { [, $path, $deferred] = $openArr; diff --git a/src/Driver/UvFile.php b/src/Driver/UvFile.php index 7e1c32a..fd3b1a5 100644 --- a/src/Driver/UvFile.php +++ b/src/Driver/UvFile.php @@ -283,7 +283,6 @@ final class UvFile implements File $deferred->fail(new StreamException("Writing to the file failed: " . $error)); } } else { - StatCache::clear($this->path); $this->position += $length; if ($this->position > $this->size) { $this->size = $this->position; @@ -312,7 +311,6 @@ final class UvFile implements File $this->isActive = false; } - StatCache::clear($this->path); $this->size = $size; $deferred->resolve(); }; diff --git a/src/Internal/Cache.php b/src/Internal/Cache.php new file mode 100644 index 0000000..8037cac --- /dev/null +++ b/src/Internal/Cache.php @@ -0,0 +1,123 @@ +sharedState = $sharedState = new class { + use Struct; + + /** @var string[] */ + public $cache = []; + /** @var int[] */ + public $cacheTimeouts = []; + /** @var bool */ + public $isSortNeeded = false; + + public function collectGarbage(): void + { + $now = \time(); + + if ($this->isSortNeeded) { + \asort($this->cacheTimeouts); + $this->isSortNeeded = false; + } + + foreach ($this->cacheTimeouts as $key => $expiry) { + if ($now <= $expiry) { + break; + } + + unset( + $this->cache[$key], + $this->cacheTimeouts[$key] + ); + } + } + }; + + $this->ttlWatcherId = Loop::repeat($gcInterval, [$sharedState, "collectGarbage"]); + $this->maxSize = $maxSize; + + Loop::unreference($this->ttlWatcherId); + } + + public function __destruct() + { + $this->sharedState->cache = []; + $this->sharedState->cacheTimeouts = []; + + Loop::cancel($this->ttlWatcherId); + } + + public function get(string $key) + { + if (!isset($this->sharedState->cache[$key])) { + return null; + } + + if (isset($this->sharedState->cacheTimeouts[$key]) && \time() > $this->sharedState->cacheTimeouts[$key]) { + unset( + $this->sharedState->cache[$key], + $this->sharedState->cacheTimeouts[$key] + ); + + return null; + } + + return $this->sharedState->cache[$key]; + } + + public function set(string $key, $value, int $ttl = null): void + { + if ($ttl === null) { + unset($this->sharedState->cacheTimeouts[$key]); + } elseif ($ttl >= 0) { + $expiry = \time() + $ttl; + $this->sharedState->cacheTimeouts[$key] = $expiry; + $this->sharedState->isSortNeeded = true; + } else { + throw new \Error("Invalid cache TTL ({$ttl}; integer >= 0 or null required"); + } + + unset($this->sharedState->cache[$key]); + if (\count($this->sharedState->cache) === $this->maxSize) { + \array_shift($this->sharedState->cache); + } + + $this->sharedState->cache[$key] = $value; + } + + public function delete(string $key): bool + { + $exists = isset($this->sharedState->cache[$key]); + + unset( + $this->sharedState->cache[$key], + $this->sharedState->cacheTimeouts[$key] + ); + + return $exists; + } +} \ No newline at end of file diff --git a/src/Internal/FileTask.php b/src/Internal/FileTask.php index bfa9e4f..5b0da53 100644 --- a/src/Internal/FileTask.php +++ b/src/Internal/FileTask.php @@ -125,8 +125,6 @@ final class FileTask implements Task } } - StatCache::clear(); - switch ($this->operation) { case "getStatus": case "deleteFile": diff --git a/src/StatCache.php b/src/StatCache.php deleted file mode 100644 index 95f11fb..0000000 --- a/src/StatCache.php +++ /dev/null @@ -1,100 +0,0 @@ - $expiry) { - if ($now > $expiry) { - unset( - self::$cache[$path], - self::$timeouts[$path] - ); - } else { - break; - } - } - }); - - Loop::unreference($watcher); - - Loop::setState(self::class, new class($watcher) { - private $watcher; - private $driver; - - public function __construct(string $watcher) - { - $this->watcher = $watcher; - $this->driver = Loop::get(); - } - - public function __destruct() - { - $this->driver->cancel($this->watcher); - } - }); - } - - public static function get(string $path): ?array - { - return isset(self::$cache[$path]) ? self::$cache[$path] : null; - } - - public static function set(string $path, array $stat): void - { - if (self::$ttl <= 0) { - return; - } - - if (Loop::getState(self::class) === null) { - self::init(); - } - - self::$cache[$path] = $stat; - self::$timeouts[$path] = self::$now + self::$ttl; - } - - public static function ttl(int $seconds): void - { - self::$ttl = $seconds; - } - - public static function clear(?string $path = null): void - { - if (isset($path)) { - unset( - self::$cache[$path], - self::$timeouts[$path] - ); - } else { - self::$cache = []; - self::$timeouts = []; - } - } - - public static function clearOn(Promise $promise, ?string $path = null): void - { - $promise->onResolve( - function (?Throwable $exception) use ($path): void { - if ($exception === null) { - self::clear($path); - } - } - ); - } -} diff --git a/src/functions.php b/src/functions.php index e970437..bce540c 100644 --- a/src/functions.php +++ b/src/functions.php @@ -5,6 +5,7 @@ namespace Amp\File; use Amp\File\Driver\BlockingDriver; use Amp\File\Driver\EioDriver; use Amp\File\Driver\ParallelDriver; +use Amp\File\Driver\StatusCachingDriver; use Amp\File\Driver\UvDriver; use Amp\Loop; use Amp\Promise; @@ -25,7 +26,13 @@ function filesystem(?Driver $driver = null): Filesystem return $filesystem; } - $filesystem = new Filesystem(createDefaultDriver()); + $defaultDriver = createDefaultDriver(); + + if (!\defined("AMP_WORKER")) { // Prevent caching in workers, cache in parent instead. + $defaultDriver = new StatusCachingDriver($defaultDriver); + } + + $filesystem = new Filesystem($defaultDriver); } else { $filesystem = new Filesystem($driver); } diff --git a/test/Driver/StatusCachingDriverTest.php b/test/Driver/StatusCachingDriverTest.php new file mode 100644 index 0000000..e76c451 --- /dev/null +++ b/test/Driver/StatusCachingDriverTest.php @@ -0,0 +1,15 @@ +driver->getStatus($path); - $this->assertNotNull(File\StatCache::get($path)); $this->assertIsArray($stat); $this->assertSameStatus(\stat($path), $stat); } @@ -172,7 +171,6 @@ abstract class DriverTest extends FilesystemTest $path = "{$fixtureDir}/file"; $stat = (yield $this->driver->getStatus($path)); $size = $stat["size"]; - File\StatCache::clear($path); $this->assertSame($size, yield $this->driver->getSize($path)); } @@ -192,7 +190,6 @@ abstract class DriverTest extends FilesystemTest $fixtureDir = Fixture::path(); $path = "{$fixtureDir}/dir"; $this->assertTrue(yield $this->driver->isDirectory($path)); - File\StatCache::clear($path); yield $this->driver->getSize($path); } @@ -300,7 +297,6 @@ abstract class DriverTest extends FilesystemTest $toUnlink = "{$fixtureDir}/unlink"; yield $this->driver->getStatus($toUnlink); yield $this->driver->write($toUnlink, "unlink me"); - $this->assertNull(File\StatCache::get($toUnlink)); $this->assertNull(yield $this->driver->deleteFile($toUnlink)); $this->assertNull(yield $this->driver->getStatus($toUnlink)); } @@ -338,7 +334,6 @@ abstract class DriverTest extends FilesystemTest $stat = yield $this->driver->getStatus($dir); $this->assertSame('0755', $this->getPermissionsFromStatus($stat)); $this->assertNull(yield $this->driver->deleteDirectory($dir)); - $this->assertNull(File\StatCache::get($dir)); $this->assertNull(yield $this->driver->getStatus($dir)); // test for 0, because previous array_filter made that not work @@ -385,7 +380,6 @@ abstract class DriverTest extends FilesystemTest $path = "{$fixtureDir}/file"; $stat = yield $this->driver->getStatus($path); $statMtime = $stat["mtime"]; - File\StatCache::clear($path); $this->assertSame($statMtime, yield $this->driver->getModificationTime($path)); } @@ -405,7 +399,6 @@ abstract class DriverTest extends FilesystemTest $path = "{$fixtureDir}/file"; $stat = yield $this->driver->getStatus($path); $statAtime = $stat["atime"]; - File\StatCache::clear($path); $this->assertSame($statAtime, yield $this->driver->getAccessTime($path)); } @@ -425,7 +418,6 @@ abstract class DriverTest extends FilesystemTest $path = "{$fixtureDir}/file"; $stat = yield $this->driver->getStatus($path); $statCtime = $stat["ctime"]; - File\StatCache::clear($path); $this->assertSame($statCtime, yield $this->driver->getCreationTime($path)); } @@ -451,7 +443,6 @@ abstract class DriverTest extends FilesystemTest $oldStat = yield $this->driver->getStatus($touch); $this->assertNull(yield $this->driver->touch($touch, \time() + 10, \time() + 20)); - $this->assertNull(File\StatCache::get($touch)); $newStat = yield $this->driver->getStatus($touch); yield $this->driver->deleteFile($touch); @@ -477,7 +468,6 @@ abstract class DriverTest extends FilesystemTest $stat = yield $this->driver->getStatus($path); $this->assertNotSame('0777', \substr(\decoct($stat['mode']), -4)); $this->assertNull(yield $this->driver->changePermissions($path, 0777)); - $this->assertNull(File\StatCache::get($path)); $stat = yield $this->driver->getStatus($path); $this->assertSame('0777', \substr(\decoct($stat['mode']), -4)); } @@ -500,7 +490,6 @@ abstract class DriverTest extends FilesystemTest yield $this->driver->getStatus($path); $user = \fileowner($path); $this->assertNull(yield $this->driver->changeOwner($path, $user, null)); - $this->assertNull(File\StatCache::get($path)); } public function testChangeOwnerFailsOnNonexistentPath(): \Generator diff --git a/test/FilesystemTest.php b/test/FilesystemTest.php index 09edfda..5ea0f86 100644 --- a/test/FilesystemTest.php +++ b/test/FilesystemTest.php @@ -11,7 +11,6 @@ abstract class FilesystemTest extends AsyncTestCase { parent::setUp(); Fixture::init(); - File\StatCache::clear(); } protected function tearDown(): void