mirror of
https://github.com/danog/file.git
synced 2025-01-22 13:21:13 +01:00
Rework stat cache to use a decorator
This commit is contained in:
parent
cbfe211760
commit
2162e7dc48
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
136
src/Driver/StatusCachingDriver.php
Normal file
136
src/Driver/StatusCachingDriver.php
Normal file
@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\File\Driver;
|
||||
|
||||
use Amp\File\Driver;
|
||||
use Amp\File\Internal\Cache;
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use function Amp\call;
|
||||
|
||||
final class StatusCachingDriver implements Driver
|
||||
{
|
||||
/** @var Driver */
|
||||
private $driver;
|
||||
|
||||
/** @var Cache */
|
||||
private $statusCache;
|
||||
|
||||
public function __construct(Driver $driver)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
81
src/Driver/StatusCachingFile.php
Normal file
81
src/Driver/StatusCachingFile.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Amp\File\Driver;
|
||||
|
||||
use Amp\File\File;
|
||||
use Amp\Promise;
|
||||
|
||||
final class StatusCachingFile implements File
|
||||
{
|
||||
/** @var File */
|
||||
private $file;
|
||||
|
||||
/** @var callable */
|
||||
private $invalidateCallback;
|
||||
|
||||
public function __construct(File $file, callable $invalidateCallback)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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();
|
||||
};
|
||||
|
123
src/Internal/Cache.php
Normal file
123
src/Internal/Cache.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\File\Internal;
|
||||
|
||||
use Amp\Loop;
|
||||
use Amp\Struct;
|
||||
|
||||
/** @internal */
|
||||
final class Cache
|
||||
{
|
||||
/** @var object */
|
||||
private $sharedState;
|
||||
/** @var string */
|
||||
private $ttlWatcherId;
|
||||
/** @var int|null */
|
||||
private $maxSize;
|
||||
|
||||
/**
|
||||
* @param int $gcInterval The frequency in milliseconds at which expired cache entries should be garbage
|
||||
* collected.
|
||||
* @param int|null $maxSize The maximum size of cache array (number of elements).
|
||||
*/
|
||||
public function __construct(int $gcInterval = 1000, int $maxSize = null)
|
||||
{
|
||||
// By using a shared state object we're able to use `__destruct()` for "normal" garbage collection of both this
|
||||
// instance and the loop's watcher. Otherwise this object could only be GC'd when the TTL watcher was cancelled
|
||||
// at the loop layer.
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -125,8 +125,6 @@ final class FileTask implements Task
|
||||
}
|
||||
}
|
||||
|
||||
StatCache::clear();
|
||||
|
||||
switch ($this->operation) {
|
||||
case "getStatus":
|
||||
case "deleteFile":
|
||||
|
@ -1,100 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\File;
|
||||
|
||||
use Amp\Loop;
|
||||
use Amp\Promise;
|
||||
use Throwable;
|
||||
|
||||
final class StatCache
|
||||
{
|
||||
private static $cache = [];
|
||||
private static $timeouts = [];
|
||||
private static $ttl = 3;
|
||||
private static $now;
|
||||
|
||||
private static function init(): void
|
||||
{
|
||||
self::$now = \time();
|
||||
|
||||
$watcher = Loop::repeat(1000, function () {
|
||||
self::$now = $now = \time();
|
||||
foreach (self::$cache as $path => $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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
15
test/Driver/StatusCachingDriverTest.php
Normal file
15
test/Driver/StatusCachingDriverTest.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\File\Test\Driver;
|
||||
|
||||
use Amp\File;
|
||||
use Amp\File\Driver\BlockingDriver;
|
||||
use Amp\File\Test\DriverTest;
|
||||
|
||||
class StatusCachingDriverTest extends DriverTest
|
||||
{
|
||||
protected function createDriver(): File\Driver
|
||||
{
|
||||
return new File\Driver\StatusCachingDriver(new BlockingDriver);
|
||||
}
|
||||
}
|
15
test/Driver/StatusCachingFileTest.php
Normal file
15
test/Driver/StatusCachingFileTest.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\File\Test\Driver;
|
||||
|
||||
use Amp\File;
|
||||
use Amp\File\Driver\BlockingDriver;
|
||||
use Amp\File\Test\FileTest;
|
||||
|
||||
class StatusCachingFileTest extends FileTest
|
||||
{
|
||||
protected function createDriver(): File\Driver
|
||||
{
|
||||
return new File\Driver\StatusCachingDriver(new BlockingDriver);
|
||||
}
|
||||
}
|
@ -133,7 +133,6 @@ abstract class DriverTest extends FilesystemTest
|
||||
$fixtureDir = Fixture::path();
|
||||
$path = "{$fixtureDir}/file";
|
||||
$stat = yield $this->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
|
||||
|
@ -11,7 +11,6 @@ abstract class FilesystemTest extends AsyncTestCase
|
||||
{
|
||||
parent::setUp();
|
||||
Fixture::init();
|
||||
File\StatCache::clear();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
|
Loading…
x
Reference in New Issue
Block a user