From 82ea1815fd2c24414e207597207d185c3c646c10 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 4 Feb 2024 14:45:32 +0100 Subject: [PATCH 01/11] Refactor cache driver --- inc/cache.php | 2 +- inc/driver/cache-driver.php | 266 ++++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 inc/driver/cache-driver.php diff --git a/inc/cache.php b/inc/cache.php index c4053610..ae788bc1 100644 --- a/inc/cache.php +++ b/inc/cache.php @@ -25,7 +25,7 @@ class Cache { self::$cache->select($config['cache']['redis'][3]) or die('cache select failure'); break; case 'php': - self::$cache = array(); + self::$cache = []; break; } } diff --git a/inc/driver/cache-driver.php b/inc/driver/cache-driver.php new file mode 100644 index 00000000..b5279d82 --- /dev/null +++ b/inc/driver/cache-driver.php @@ -0,0 +1,266 @@ +setOption(Memcached::OPT_BINARY_PROTOCOL, true)) { + throw new RuntimeException('Unable to set the memcached protocol!'); + } + if (!$memcached->setOption(Memcached::OPT_PREFIX_KEY, $prefix)) { + throw new RuntimeException('Unable to set the memcached prefix!'); + } + if (!$memcached->addServers($memcached_server)) { + throw new RuntimeException('Unable to add the memcached server!'); + } + + return new class($memcached) implements CacheDriver { + private Memcached $inner; + + public function __construct(Memcached $inner) { + $this->inner = $inner; + } + + public function get(string $key): mixed { + $ret = $this->inner->get($key); + // If the returned value is false but the retrival was a success, then the value stored was a boolean false. + if ($ret === false && $this->inner->getResultCode() !== Memcached::RES_SUCCESS) { + return null; + } + return $ret; + } + + public function set(string $key, mixed $value, mixed $expires = false): void { + $this->inner->set($key, $value, (int)$expires); + } + + public function delete(string $key): void { + $this->inner->delete($key); + } + + public function flush(): void { + $this->inner->flush(); + } + }; + } + + public static function redis(string $prefix, string $host, int $port, string $password, string $database) { + $redis = new Redis(); + $redis->connect($host, $port); + if ($password) { + $redis->auth($password); + } + if (!$redis->select($database)) { + throw new RuntimeException('Unable to connect to Redis!'); + } + + return new class($prefix, $redis) implements CacheDriver { + private string $prefix; + private Redis $inner; + + public function __construct(string $prefix, Redis $inner) { + $$this->prefix = $prefix; + $this->inner = $inner; + } + + public function get(string $key): mixed { + $ret = $this->inner->get($this->prefix . $key); + if ($ret === false) { + return null; + } + return json_decode($ret, true); + } + + public function set(string $key, mixed $value, mixed $expires = false): void { + if ($expires === false) { + $this->inner->set($this->prefix . $key, json_encode($value)); + } else { + $expires = $expires * 1000; // Seconds to milliseconds. + $this->inner->setex($this->prefix . $key, $expires, json_encode($value)); + } + } + + public function delete(string $key): void { + $this->inner->del($this->prefix . $key); + } + + public function flush(): void { + $this->inner->flushDB(); + } + }; + } + + public static function apcu() { + return new class implements CacheDriver { + public function get(string $key): mixed { + $success = false; + $ret = apcu_fetch($key, $success); + if ($success === false) { + return null; + } + return $ret; + } + + public function set(string $key, mixed $value, mixed $expires = false): void { + apcu_store($key, $value, (int)$expires); + } + + public function delete(string $key): void { + apcu_delete($key); + } + + public function flush(): void { + apcu_clear_cache(); + } + }; + } + + public static function filesystem(string $prefix, string $base_path) { + if ($base_path[strlen($base_path) - 1] !== '/') { + $base_path = "$base_path/"; + } + + if (!is_dir($base_path)) { + throw new RuntimeException("$base_path is not a directory!"); + } + + if (!is_writable($base_path)) { + throw new RuntimeException("$base_path is not writable!"); + } + + return new class($prefix, $base_path) implements CacheDriver { + private string $prefix; + private string $base_path; + + + private function prepareKey(string $key): string { + $key = str_replace('/', '::', $key); + $key = str_replace("\0", '', $key); + return $this->prefix . $key; + } + + public function __construct(string $prefix, string $base_path) { + $this->prefix = $prefix; + $this->base_path = $base_path; + } + + public function get(string $key): mixed { + $key = $this->prepareKey($key); + + $fd = fopen("$this->base_path/$key", 'r'); + if ($fd === false) { + return null; + } + + $data = stream_get_contents("$this->base_path/$key"); + fclose($fd); + return json_decode($data, true); + } + + public function set(string $key, mixed $value, mixed $expires = false): void { + $key = $this->prepareKey($key); + + $data = json_encode($value); + file_put_contents("$this->base_path/$key", $data); + } + + public function delete(string $key): void { + $key = $this->prepareKey($key); + + @unlink("$this->base_path/$key"); + } + + public function flush(): void { + $files = glob("$this->base_path/$this->prefix*"); + foreach ($files as $file) { + @unlink($file); + } + } + }; + } + + public static function phpArray() { + return new class implements CacheDriver { + private static array $inner = []; + + public function get(string $key): mixed { + return isset(self::$inner[$key]) ? self::$inner[$key] : null; + } + + public function set(string $key, mixed $value, mixed $expires = false): void { + self::$inner[$key] = $value; + } + + public function delete(string $key): void { + unset(self::$inner[$key]); + } + + public function flush(): void { + self::$inner = []; + } + }; + } + + /** + * No-op cache. Useful for testing. + */ + public static function none() { + return new class implements CacheDriver { + public function get(string $key): mixed { + return null; + } + + public function set(string $key, mixed $value, mixed $expires = false): void { + // No-op. + } + + public function delete(string $key): void { + // No-op. + } + + public function flush(): void { + // No-op. + } + }; + } +} + +interface CacheDriver { + /** + * Get the value of associated with the key. + * + * @param string $key The key of the value. + * @return mixed|null The value associated with the key, or null if there is none. + */ + public function get(string $key): mixed; + + /** + * Set a key-value pair. + * + * @param string $key The key. + * @param mixed $value The value. + * @param int|false $expires After how many seconds the pair will expire. Use false or ignore this parameter to keep + * the value until it gets evicted to make space for more items. Some drivers will always + * ignore this parameter and store the pair until it's removed. + */ + public function set(string $key, mixed $value, mixed $expires = false): void; + + /** + * Delete a key-value pair. + * + * @param string $key The key. + */ + public function delete(string $key): void; + + /** + * Delete all the key-value pairs. + */ + public function flush(): void; +} From b57d9bfbb3b581cf63f34c831c760b632d903c8c Mon Sep 17 00:00:00 2001 From: Zankaria Date: Thu, 11 Apr 2024 19:20:19 +0200 Subject: [PATCH 02/11] cache: implement cache locking for filesystem cache and give it multiprocess support --- inc/driver/cache-driver.php | 55 +++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/inc/driver/cache-driver.php b/inc/driver/cache-driver.php index b5279d82..0718ad86 100644 --- a/inc/driver/cache-driver.php +++ b/inc/driver/cache-driver.php @@ -122,7 +122,7 @@ class CacheDrivers { }; } - public static function filesystem(string $prefix, string $base_path) { + public static function filesystem(string $prefix, string $base_path, string $lock_file) { if ($base_path[strlen($base_path) - 1] !== '/') { $base_path = "$base_path/"; } @@ -135,9 +135,17 @@ class CacheDrivers { throw new RuntimeException("$base_path is not writable!"); } - return new class($prefix, $base_path) implements CacheDriver { + $lock_file = $base_path . $lock_file; + + $lock_fd = fopen($lock_file, 'w'); + if ($lock_fd === false) { + throw new RuntimeException('Unable to open the lock file!'); + } + + $ret = new class($prefix, $base_path, $lock_file) implements CacheDriver { private string $prefix; private string $base_path; + private mixed $lock_fd; private function prepareKey(string $key): string { @@ -146,21 +154,38 @@ class CacheDrivers { return $this->prefix . $key; } - public function __construct(string $prefix, string $base_path) { + private function sharedLockCache(): void { + flock($this->lock_fd, LOCK_SH); + } + + private function exclusiveLockCache(): void { + flock($this->lock_fd, LOCK_EX); + } + + private function unlockCache(): void { + flock($this->lock_fd, LOCK_UN); + } + + public function __construct(string $prefix, string $base_path, mixed $lock_fd) { $this->prefix = $prefix; $this->base_path = $base_path; + $this->lock_fd = $lock_fd; } public function get(string $key): mixed { $key = $this->prepareKey($key); - $fd = fopen("$this->base_path/$key", 'r'); + $this->sharedLockCache(); + + $fd = fopen($this->base_path . $key, 'r'); if ($fd === false) { + $this->unlockCache(); return null; } - $data = stream_get_contents("$this->base_path/$key"); + $data = stream_get_contents($fd); fclose($fd); + $this->unlockCache(); return json_decode($data, true); } @@ -168,22 +193,36 @@ class CacheDrivers { $key = $this->prepareKey($key); $data = json_encode($value); - file_put_contents("$this->base_path/$key", $data); + $this->exclusiveLockCache(); + file_put_contents($this->base_path . $key, $data); + $this->unlockCache(); } public function delete(string $key): void { $key = $this->prepareKey($key); - @unlink("$this->base_path/$key"); + $this->exclusiveLockCache(); + @unlink($this->base_path . $key); + $this->unlockCache(); } public function flush(): void { - $files = glob("$this->base_path/$this->prefix*"); + $this->exclusiveLockCache(); + $files = glob($this->base_path . $this->prefix . '*', GLOB_NOSORT); foreach ($files as $file) { @unlink($file); } + $this->unlockCache(); + } + + public function close() { + fclose($this->lock_fd); } }; + + register_shutdown_function([$ret, 'close']); + + return $ret; } public static function phpArray() { From 5ea42fa0e2bb3c61c711a0461774e5d3b1b8f875 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Thu, 11 Apr 2024 18:19:18 +0200 Subject: [PATCH 03/11] config.php: update cache documentation --- inc/config.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/inc/config.php b/inc/config.php index 11ac8809..d958d617 100644 --- a/inc/config.php +++ b/inc/config.php @@ -139,17 +139,26 @@ /* * On top of the static file caching system, you can enable the additional caching system which is - * designed to minimize SQL queries and can significantly increase speed when posting or using the - * moderator interface. APC is the recommended method of caching. + * designed to minimize request processing can significantly increase speed when posting or using + * the moderator interface. * * https://github.com/vichan-devel/vichan/wiki/cache */ + // Uses a PHP array. MUST NOT be used in multiprocess environments. $config['cache']['enabled'] = 'php'; + // The recommended in-memory method of caching. Requires the extension. Due to how APCu works, this should be + // disabled when you run tools from the cli. // $config['cache']['enabled'] = 'apcu'; + // The Memcache server. Requires the memcached extension, with a final D. // $config['cache']['enabled'] = 'memcached'; + // The Redis server. Requires the extension. // $config['cache']['enabled'] = 'redis'; + // Use the local cache folder. Slower than native but available out of the box and compatible with multiprocess + // environments. You can mount a ram-based filesystem in the cache directory to improve performance. // $config['cache']['enabled'] = 'fs'; + // Technically available, offers a no-op fake cache. Don't use this outside of testing or debugging. + // $config['cache']['enabled'] = 'none'; // Timeout for cached objects such as posts and HTML. $config['cache']['timeout'] = 60 * 60 * 48; // 48 hours From 66d2f901718dcdb3e88cb9683b3cd02e5f9a3100 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 20 Sep 2024 19:44:50 +0200 Subject: [PATCH 04/11] cache-driver.php: filesystem handle expired values. --- inc/driver/cache-driver.php | 55 ++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/inc/driver/cache-driver.php b/inc/driver/cache-driver.php index 0718ad86..672d7b92 100644 --- a/inc/driver/cache-driver.php +++ b/inc/driver/cache-driver.php @@ -122,7 +122,7 @@ class CacheDrivers { }; } - public static function filesystem(string $prefix, string $base_path, string $lock_file) { + public static function filesystem(string $prefix, string $base_path, string $lock_file, int|false $collect_chance_den) { if ($base_path[strlen($base_path) - 1] !== '/') { $base_path = "$base_path/"; } @@ -146,6 +146,7 @@ class CacheDrivers { private string $prefix; private string $base_path; private mixed $lock_fd; + private int|false $collect_chance_den; private function prepareKey(string $key): string { @@ -166,10 +167,34 @@ class CacheDrivers { flock($this->lock_fd, LOCK_UN); } - public function __construct(string $prefix, string $base_path, mixed $lock_fd) { + private function collectImpl(): int { + // A read lock is ok, since it's alright if we delete expired items from under the feet of other processes. + $files = glob($this->base_path . $this->prefix . '*', GLOB_NOSORT); + $count = 0; + foreach ($files as $file) { + $data = file_get_contents($file); + $wrapped = json_decode($data, true, 512, JSON_THROW_ON_ERROR); + if ($wrapped['expires'] !== false && $wrapped['expires'] <= time()) { + if (@unlink($file)) { + $count++; + } + } + } + return $count; + } + + private function maybeCollect() { + if ($this->collect_chance_den !== false && rand(0, $this->collect_chance_den) === 0) { + $this->collect_chance_den = false; // Collect only once per instance (aka process$this->collect_chance_den !== false$this->collect_chance_den !== false). + $this->collectImpl(); + } + } + + public function __construct(string $prefix, string $base_path, mixed $lock_fd, int|false $collect_chance_den) { $this->prefix = $prefix; $this->base_path = $base_path; $this->lock_fd = $lock_fd; + $this->collect_chance_den = $collect_chance_den; } public function get(string $key): mixed { @@ -185,16 +210,30 @@ class CacheDrivers { $data = stream_get_contents($fd); fclose($fd); + $this->maybeCollect(); $this->unlockCache(); - return json_decode($data, true); + $wrapped = json_decode($data, true, 512, JSON_THROW_ON_ERROR); + + if ($wrapped['expires'] !== false && $wrapped['expires'] <= time()) { + // Already, expired, pretend it doesn't exist. + return null; + } else { + return $wrapped['inner']; + } } public function set(string $key, mixed $value, mixed $expires = false): void { $key = $this->prepareKey($key); - $data = json_encode($value); + $wrapped = [ + 'expires' => $expires ? time() + $expires : false, + 'inner' => $value + ]; + + $data = json_encode($wrapped); $this->exclusiveLockCache(); file_put_contents($this->base_path . $key, $data); + $this->maybeCollect(); $this->unlockCache(); } @@ -203,9 +242,17 @@ class CacheDrivers { $this->exclusiveLockCache(); @unlink($this->base_path . $key); + $this->maybeCollect(); $this->unlockCache(); } + public function collect() { + $this->sharedLockCache(); + $count = $this->collectImpl(); + $this->unlockCache(); + return $count; + } + public function flush(): void { $this->exclusiveLockCache(); $files = glob($this->base_path . $this->prefix . '*', GLOB_NOSORT); From 3d406aeab2343c23b694d5d84b20fc359a7254e5 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 2 Oct 2024 21:36:28 +0200 Subject: [PATCH 05/11] cache-driver.php: move to Data --- inc/{driver => Data/Driver}/cache-driver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename inc/{driver => Data/Driver}/cache-driver.php (99%) diff --git a/inc/driver/cache-driver.php b/inc/Data/Driver/cache-driver.php similarity index 99% rename from inc/driver/cache-driver.php rename to inc/Data/Driver/cache-driver.php index 672d7b92..b943ee71 100644 --- a/inc/driver/cache-driver.php +++ b/inc/Data/Driver/cache-driver.php @@ -1,5 +1,5 @@ Date: Wed, 2 Oct 2024 21:49:51 +0200 Subject: [PATCH 06/11] driver: break up cache drivers --- inc/Data/Driver/ApcuCacheDriver.php | 28 ++ inc/Data/Driver/ArrayCacheDriver.php | 28 ++ inc/Data/Driver/CacheDriver.php | 38 +++ inc/Data/Driver/FsCachedriver.php | 150 ++++++++++ inc/Data/Driver/MemcacheCacheDriver.php | 43 +++ inc/Data/Driver/NoneCacheDriver.php | 26 ++ inc/Data/Driver/RedisCacheDriver.php | 48 ++++ inc/Data/Driver/cache-driver.php | 352 ------------------------ 8 files changed, 361 insertions(+), 352 deletions(-) create mode 100644 inc/Data/Driver/ApcuCacheDriver.php create mode 100644 inc/Data/Driver/ArrayCacheDriver.php create mode 100644 inc/Data/Driver/CacheDriver.php create mode 100644 inc/Data/Driver/FsCachedriver.php create mode 100644 inc/Data/Driver/MemcacheCacheDriver.php create mode 100644 inc/Data/Driver/NoneCacheDriver.php create mode 100644 inc/Data/Driver/RedisCacheDriver.php delete mode 100644 inc/Data/Driver/cache-driver.php diff --git a/inc/Data/Driver/ApcuCacheDriver.php b/inc/Data/Driver/ApcuCacheDriver.php new file mode 100644 index 00000000..a39bb656 --- /dev/null +++ b/inc/Data/Driver/ApcuCacheDriver.php @@ -0,0 +1,28 @@ +prefix . $key; + } + + private function sharedLockCache(): void { + \flock($this->lock_fd, LOCK_SH); + } + + private function exclusiveLockCache(): void { + \flock($this->lock_fd, LOCK_EX); + } + + private function unlockCache(): void { + \flock($this->lock_fd, LOCK_UN); + } + + private function collectImpl(): int { + // A read lock is ok, since it's alright if we delete expired items from under the feet of other processes. + $files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT); + $count = 0; + foreach ($files as $file) { + $data = \file_get_contents($file); + $wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR); + if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) { + if (@\unlink($file)) { + $count++; + } + } + } + return $count; + } + + private function maybeCollect(): void { + if ($this->collect_chance_den !== false && \mt_rand(0, $this->collect_chance_den - 1) === 0) { + $this->collect_chance_den = false; // Collect only once per instance (aka process). + $this->collectImpl(); + } + } + + public function __construct(string $prefix, string $base_path, string $lock_file, int|false $collect_chance_den) { + if ($base_path[\strlen($base_path) - 1] !== '/') { + $base_path = "$base_path/"; + } + + if (!\is_dir($base_path)) { + throw new \RuntimeException("$base_path is not a directory!"); + } + + if (!\is_writable($base_path)) { + throw new \RuntimeException("$base_path is not writable!"); + } + + $this->lock_fd = \fopen($base_path . $lock_file, 'w'); + if ($this->lock_fd === false) { + throw new \RuntimeException('Unable to open the lock file!'); + } + + $this->prefix = $prefix; + $this->base_path = $base_path; + $this->collect_chance_den = $collect_chance_den; + } + + public function __destruct() { + $this->close(); + } + + public function get(string $key): mixed { + $key = $this->prepareKey($key); + + $this->sharedLockCache(); + + $fd = \fopen($this->base_path . $key, 'r'); + if ($fd === false) { + $this->unlockCache(); + return null; + } + + $data = \stream_get_contents($fd); + \fclose($fd); + $this->maybeCollect(); + $this->unlockCache(); + $wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR); + + if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) { + // Already expired, leave it there since we already released the lock and pretend it doesn't exist. + return null; + } else { + return $wrapped['inner']; + } + } + + public function set(string $key, mixed $value, mixed $expires = false): void { + $key = $this->prepareKey($key); + + $wrapped = [ + 'expires' => $expires ? \time() + $expires : false, + 'inner' => $value + ]; + + $data = \json_encode($wrapped); + $this->exclusiveLockCache(); + \file_put_contents($this->base_path . $key, $data); + $this->maybeCollect(); + $this->unlockCache(); + } + + public function delete(string $key): void { + $key = $this->prepareKey($key); + + $this->exclusiveLockCache(); + @\unlink($this->base_path . $key); + $this->maybeCollect(); + $this->unlockCache(); + } + + public function collect(): int { + $this->sharedLockCache(); + $count = $this->collectImpl(); + $this->unlockCache(); + return $count; + } + + public function flush(): void { + $this->exclusiveLockCache(); + $files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT); + foreach ($files as $file) { + @\unlink($file); + } + $this->unlockCache(); + } + + public function close(): void { + \fclose($this->lock_fd); + } +} diff --git a/inc/Data/Driver/MemcacheCacheDriver.php b/inc/Data/Driver/MemcacheCacheDriver.php new file mode 100644 index 00000000..04f62895 --- /dev/null +++ b/inc/Data/Driver/MemcacheCacheDriver.php @@ -0,0 +1,43 @@ +inner = new \Memcached(); + if (!$this->inner->setOption(\Memcached::OPT_BINARY_PROTOCOL, true)) { + throw new \RuntimeException('Unable to set the memcached protocol!'); + } + if (!$this->inner->setOption(\Memcached::OPT_PREFIX_KEY, $prefix)) { + throw new \RuntimeException('Unable to set the memcached prefix!'); + } + if (!$this->inner->addServers($memcached_server)) { + throw new \RuntimeException('Unable to add the memcached server!'); + } + } + + public function get(string $key): mixed { + $ret = $this->inner->get($key); + // If the returned value is false but the retrival was a success, then the value stored was a boolean false. + if ($ret === false && $this->inner->getResultCode() !== \Memcached::RES_SUCCESS) { + return null; + } + return $ret; + } + + public function set(string $key, mixed $value, mixed $expires = false): void { + $this->inner->set($key, $value, (int)$expires); + } + + public function delete(string $key): void { + $this->inner->delete($key); + } + + public function flush(): void { + $this->inner->flush(); + } +} diff --git a/inc/Data/Driver/NoneCacheDriver.php b/inc/Data/Driver/NoneCacheDriver.php new file mode 100644 index 00000000..8b260a50 --- /dev/null +++ b/inc/Data/Driver/NoneCacheDriver.php @@ -0,0 +1,26 @@ +inner = new \Redis(); + $this->inner->connect($host, $port); + if ($password) { + $this->inner->auth($password); + } + if (!$this->inner->select($database)) { + throw new \RuntimeException('Unable to connect to Redis!'); + } + + $$this->prefix = $prefix; + } + + public function get(string $key): mixed { + $ret = $this->inner->get($this->prefix . $key); + if ($ret === false) { + return null; + } + return \json_decode($ret, true); + } + + public function set(string $key, mixed $value, mixed $expires = false): void { + if ($expires === false) { + $this->inner->set($this->prefix . $key, \json_encode($value)); + } else { + $expires = $expires * 1000; // Seconds to milliseconds. + $this->inner->setex($this->prefix . $key, $expires, \json_encode($value)); + } + } + + public function delete(string $key): void { + $this->inner->del($this->prefix . $key); + } + + public function flush(): void { + $this->inner->flushDB(); + } +} diff --git a/inc/Data/Driver/cache-driver.php b/inc/Data/Driver/cache-driver.php deleted file mode 100644 index b943ee71..00000000 --- a/inc/Data/Driver/cache-driver.php +++ /dev/null @@ -1,352 +0,0 @@ -setOption(Memcached::OPT_BINARY_PROTOCOL, true)) { - throw new RuntimeException('Unable to set the memcached protocol!'); - } - if (!$memcached->setOption(Memcached::OPT_PREFIX_KEY, $prefix)) { - throw new RuntimeException('Unable to set the memcached prefix!'); - } - if (!$memcached->addServers($memcached_server)) { - throw new RuntimeException('Unable to add the memcached server!'); - } - - return new class($memcached) implements CacheDriver { - private Memcached $inner; - - public function __construct(Memcached $inner) { - $this->inner = $inner; - } - - public function get(string $key): mixed { - $ret = $this->inner->get($key); - // If the returned value is false but the retrival was a success, then the value stored was a boolean false. - if ($ret === false && $this->inner->getResultCode() !== Memcached::RES_SUCCESS) { - return null; - } - return $ret; - } - - public function set(string $key, mixed $value, mixed $expires = false): void { - $this->inner->set($key, $value, (int)$expires); - } - - public function delete(string $key): void { - $this->inner->delete($key); - } - - public function flush(): void { - $this->inner->flush(); - } - }; - } - - public static function redis(string $prefix, string $host, int $port, string $password, string $database) { - $redis = new Redis(); - $redis->connect($host, $port); - if ($password) { - $redis->auth($password); - } - if (!$redis->select($database)) { - throw new RuntimeException('Unable to connect to Redis!'); - } - - return new class($prefix, $redis) implements CacheDriver { - private string $prefix; - private Redis $inner; - - public function __construct(string $prefix, Redis $inner) { - $$this->prefix = $prefix; - $this->inner = $inner; - } - - public function get(string $key): mixed { - $ret = $this->inner->get($this->prefix . $key); - if ($ret === false) { - return null; - } - return json_decode($ret, true); - } - - public function set(string $key, mixed $value, mixed $expires = false): void { - if ($expires === false) { - $this->inner->set($this->prefix . $key, json_encode($value)); - } else { - $expires = $expires * 1000; // Seconds to milliseconds. - $this->inner->setex($this->prefix . $key, $expires, json_encode($value)); - } - } - - public function delete(string $key): void { - $this->inner->del($this->prefix . $key); - } - - public function flush(): void { - $this->inner->flushDB(); - } - }; - } - - public static function apcu() { - return new class implements CacheDriver { - public function get(string $key): mixed { - $success = false; - $ret = apcu_fetch($key, $success); - if ($success === false) { - return null; - } - return $ret; - } - - public function set(string $key, mixed $value, mixed $expires = false): void { - apcu_store($key, $value, (int)$expires); - } - - public function delete(string $key): void { - apcu_delete($key); - } - - public function flush(): void { - apcu_clear_cache(); - } - }; - } - - public static function filesystem(string $prefix, string $base_path, string $lock_file, int|false $collect_chance_den) { - if ($base_path[strlen($base_path) - 1] !== '/') { - $base_path = "$base_path/"; - } - - if (!is_dir($base_path)) { - throw new RuntimeException("$base_path is not a directory!"); - } - - if (!is_writable($base_path)) { - throw new RuntimeException("$base_path is not writable!"); - } - - $lock_file = $base_path . $lock_file; - - $lock_fd = fopen($lock_file, 'w'); - if ($lock_fd === false) { - throw new RuntimeException('Unable to open the lock file!'); - } - - $ret = new class($prefix, $base_path, $lock_file) implements CacheDriver { - private string $prefix; - private string $base_path; - private mixed $lock_fd; - private int|false $collect_chance_den; - - - private function prepareKey(string $key): string { - $key = str_replace('/', '::', $key); - $key = str_replace("\0", '', $key); - return $this->prefix . $key; - } - - private function sharedLockCache(): void { - flock($this->lock_fd, LOCK_SH); - } - - private function exclusiveLockCache(): void { - flock($this->lock_fd, LOCK_EX); - } - - private function unlockCache(): void { - flock($this->lock_fd, LOCK_UN); - } - - private function collectImpl(): int { - // A read lock is ok, since it's alright if we delete expired items from under the feet of other processes. - $files = glob($this->base_path . $this->prefix . '*', GLOB_NOSORT); - $count = 0; - foreach ($files as $file) { - $data = file_get_contents($file); - $wrapped = json_decode($data, true, 512, JSON_THROW_ON_ERROR); - if ($wrapped['expires'] !== false && $wrapped['expires'] <= time()) { - if (@unlink($file)) { - $count++; - } - } - } - return $count; - } - - private function maybeCollect() { - if ($this->collect_chance_den !== false && rand(0, $this->collect_chance_den) === 0) { - $this->collect_chance_den = false; // Collect only once per instance (aka process$this->collect_chance_den !== false$this->collect_chance_den !== false). - $this->collectImpl(); - } - } - - public function __construct(string $prefix, string $base_path, mixed $lock_fd, int|false $collect_chance_den) { - $this->prefix = $prefix; - $this->base_path = $base_path; - $this->lock_fd = $lock_fd; - $this->collect_chance_den = $collect_chance_den; - } - - public function get(string $key): mixed { - $key = $this->prepareKey($key); - - $this->sharedLockCache(); - - $fd = fopen($this->base_path . $key, 'r'); - if ($fd === false) { - $this->unlockCache(); - return null; - } - - $data = stream_get_contents($fd); - fclose($fd); - $this->maybeCollect(); - $this->unlockCache(); - $wrapped = json_decode($data, true, 512, JSON_THROW_ON_ERROR); - - if ($wrapped['expires'] !== false && $wrapped['expires'] <= time()) { - // Already, expired, pretend it doesn't exist. - return null; - } else { - return $wrapped['inner']; - } - } - - public function set(string $key, mixed $value, mixed $expires = false): void { - $key = $this->prepareKey($key); - - $wrapped = [ - 'expires' => $expires ? time() + $expires : false, - 'inner' => $value - ]; - - $data = json_encode($wrapped); - $this->exclusiveLockCache(); - file_put_contents($this->base_path . $key, $data); - $this->maybeCollect(); - $this->unlockCache(); - } - - public function delete(string $key): void { - $key = $this->prepareKey($key); - - $this->exclusiveLockCache(); - @unlink($this->base_path . $key); - $this->maybeCollect(); - $this->unlockCache(); - } - - public function collect() { - $this->sharedLockCache(); - $count = $this->collectImpl(); - $this->unlockCache(); - return $count; - } - - public function flush(): void { - $this->exclusiveLockCache(); - $files = glob($this->base_path . $this->prefix . '*', GLOB_NOSORT); - foreach ($files as $file) { - @unlink($file); - } - $this->unlockCache(); - } - - public function close() { - fclose($this->lock_fd); - } - }; - - register_shutdown_function([$ret, 'close']); - - return $ret; - } - - public static function phpArray() { - return new class implements CacheDriver { - private static array $inner = []; - - public function get(string $key): mixed { - return isset(self::$inner[$key]) ? self::$inner[$key] : null; - } - - public function set(string $key, mixed $value, mixed $expires = false): void { - self::$inner[$key] = $value; - } - - public function delete(string $key): void { - unset(self::$inner[$key]); - } - - public function flush(): void { - self::$inner = []; - } - }; - } - - /** - * No-op cache. Useful for testing. - */ - public static function none() { - return new class implements CacheDriver { - public function get(string $key): mixed { - return null; - } - - public function set(string $key, mixed $value, mixed $expires = false): void { - // No-op. - } - - public function delete(string $key): void { - // No-op. - } - - public function flush(): void { - // No-op. - } - }; - } -} - -interface CacheDriver { - /** - * Get the value of associated with the key. - * - * @param string $key The key of the value. - * @return mixed|null The value associated with the key, or null if there is none. - */ - public function get(string $key): mixed; - - /** - * Set a key-value pair. - * - * @param string $key The key. - * @param mixed $value The value. - * @param int|false $expires After how many seconds the pair will expire. Use false or ignore this parameter to keep - * the value until it gets evicted to make space for more items. Some drivers will always - * ignore this parameter and store the pair until it's removed. - */ - public function set(string $key, mixed $value, mixed $expires = false): void; - - /** - * Delete a key-value pair. - * - * @param string $key The key. - */ - public function delete(string $key): void; - - /** - * Delete all the key-value pairs. - */ - public function flush(): void; -} From f138b4b88799f051a5a60bc70c806d600ed25263 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 26 Apr 2024 13:55:58 +0200 Subject: [PATCH 07/11] cache.php: wrap new cache drivers --- inc/cache.php | 177 +++++++++++++++----------------------------------- 1 file changed, 51 insertions(+), 126 deletions(-) diff --git a/inc/cache.php b/inc/cache.php index ae788bc1..293660fd 100644 --- a/inc/cache.php +++ b/inc/cache.php @@ -4,164 +4,89 @@ * Copyright (c) 2010-2013 Tinyboard Development Group */ +use Vichan\Data\Driver\{CacheDriver, ApcuCacheDriver, ArrayCacheDriver, FsCacheDriver, MemcachedCacheDriver, NoneCacheDriver, RedisCacheDriver}; + defined('TINYBOARD') or exit; + class Cache { - private static $cache; - public static function init() { + private static function buildCache(): CacheDriver { global $config; switch ($config['cache']['enabled']) { case 'memcached': - self::$cache = new Memcached(); - self::$cache->addServers($config['cache']['memcached']); - break; + return new MemcachedCacheDriver( + $config['cache']['prefix'], + $config['cache']['memcached'] + ); case 'redis': - self::$cache = new Redis(); - self::$cache->connect($config['cache']['redis'][0], $config['cache']['redis'][1]); - if ($config['cache']['redis'][2]) { - self::$cache->auth($config['cache']['redis'][2]); - } - self::$cache->select($config['cache']['redis'][3]) or die('cache select failure'); - break; + return new RedisCacheDriver( + $config['cache']['prefix'], + $config['cache']['redis'][0], + $config['cache']['redis'][1], + $config['cache']['redis'][2], + $config['cache']['redis'][3] + ); + case 'apcu': + return new ApcuCacheDriver; + case 'fs': + return new FsCacheDriver( + $config['cache']['prefix'], + "tmp/cache/{$config['cache']['prefix']}", + '.lock', + $config['auto_maintenance'] ? 1000 : false + ); + case 'none': + return new NoneCacheDriver(); case 'php': - self::$cache = []; - break; + default: + return new ArrayCacheDriver(); } } + + public static function getCache(): CacheDriver { + static $cache; + return $cache ??= self::buildCache(); + } + public static function get($key) { global $config, $debug; - $key = $config['cache']['prefix'] . $key; - - $data = false; - switch ($config['cache']['enabled']) { - case 'memcached': - if (!self::$cache) - self::init(); - $data = self::$cache->get($key); - break; - case 'apcu': - $data = apcu_fetch($key); - break; - case 'php': - $data = isset(self::$cache[$key]) ? self::$cache[$key] : false; - break; - case 'fs': - $key = str_replace('/', '::', $key); - $key = str_replace("\0", '', $key); - if (!file_exists('tmp/cache/'.$key)) { - $data = false; - } - else { - $data = file_get_contents('tmp/cache/'.$key); - $data = json_decode($data, true); - } - break; - case 'redis': - if (!self::$cache) - self::init(); - $data = json_decode(self::$cache->get($key), true); - break; + $ret = self::getCache()->get($key); + if ($ret === null) { + $ret = false; } - if ($config['debug']) - $debug['cached'][] = $key . ($data === false ? ' (miss)' : ' (hit)'); + if ($config['debug']) { + $debug['cached'][] = $config['cache']['prefix'] . $key . ($ret === false ? ' (miss)' : ' (hit)'); + } - return $data; + return $ret; } public static function set($key, $value, $expires = false) { global $config, $debug; - $key = $config['cache']['prefix'] . $key; - - if (!$expires) + if (!$expires) { $expires = $config['cache']['timeout']; - - switch ($config['cache']['enabled']) { - case 'memcached': - if (!self::$cache) - self::init(); - self::$cache->set($key, $value, $expires); - break; - case 'redis': - if (!self::$cache) - self::init(); - self::$cache->setex($key, $expires, json_encode($value)); - break; - case 'apcu': - apcu_store($key, $value, $expires); - break; - case 'fs': - $key = str_replace('/', '::', $key); - $key = str_replace("\0", '', $key); - file_put_contents('tmp/cache/'.$key, json_encode($value)); - break; - case 'php': - self::$cache[$key] = $value; - break; } - if ($config['debug']) - $debug['cached'][] = $key . ' (set)'; + self::getCache()->set($key, $value, $expires); + + if ($config['debug']) { + $debug['cached'][] = $config['cache']['prefix'] . $key . ' (set)'; + } } public static function delete($key) { global $config, $debug; - $key = $config['cache']['prefix'] . $key; + self::getCache()->delete($key); - switch ($config['cache']['enabled']) { - case 'memcached': - if (!self::$cache) - self::init(); - self::$cache->delete($key); - break; - case 'redis': - if (!self::$cache) - self::init(); - self::$cache->del($key); - break; - case 'apcu': - apcu_delete($key); - break; - case 'fs': - $key = str_replace('/', '::', $key); - $key = str_replace("\0", '', $key); - @unlink('tmp/cache/'.$key); - break; - case 'php': - unset(self::$cache[$key]); - break; + if ($config['debug']) { + $debug['cached'][] = $config['cache']['prefix'] . $key . ' (deleted)'; } - - if ($config['debug']) - $debug['cached'][] = $key . ' (deleted)'; } public static function flush() { - global $config; - - switch ($config['cache']['enabled']) { - case 'memcached': - if (!self::$cache) - self::init(); - return self::$cache->flush(); - case 'apcu': - return apcu_clear_cache('user'); - case 'php': - self::$cache = array(); - break; - case 'fs': - $files = glob('tmp/cache/*'); - foreach ($files as $file) { - unlink($file); - } - break; - case 'redis': - if (!self::$cache) - self::init(); - return self::$cache->flushDB(); - } - + self::getCache()->flush(); return false; } } From 589435b667626a43c9f79df45621ed23f0b919a8 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 7 Apr 2024 21:10:39 +0200 Subject: [PATCH 08/11] context.php: use shared cache driver --- inc/context.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inc/context.php b/inc/context.php index c3ebef04..e747a68d 100644 --- a/inc/context.php +++ b/inc/context.php @@ -83,6 +83,10 @@ function build_context(array $config): Context { $config['captcha']['native']['provider_check'], $config['captcha']['native']['extra'] ); + }, + CacheDriver::class => function($c) { + // Use the global for backwards compatibility. + return \cache::getCache(); } ]); } From 243e4894fa61917bce70dd58409f2a6aa61d09b8 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sun, 7 Apr 2024 23:14:31 +0200 Subject: [PATCH 09/11] Use CacheDriver and Context for mod.php and mod pages --- inc/mod/pages.php | 88 ++++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/inc/mod/pages.php b/inc/mod/pages.php index ef41da70..8a549481 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -4,8 +4,8 @@ */ use Vichan\Context; use Vichan\Functions\Format; - use Vichan\Functions\Net; +use Vichan\Data\Driver\CacheDriver; defined('TINYBOARD') or exit; @@ -112,25 +112,23 @@ function mod_dashboard(Context $ctx) { $args['boards'] = listBoards(); if (hasPermission($config['mod']['noticeboard'])) { - if (!$config['cache']['enabled'] || !$args['noticeboard'] = cache::get('noticeboard_preview')) { + if (!$args['noticeboard'] = $ctx->get(CacheDriver::class)->get('noticeboard_preview')) { $query = prepare("SELECT ``noticeboard``.*, `username` FROM ``noticeboard`` LEFT JOIN ``mods`` ON ``mods``.`id` = `mod` ORDER BY `id` DESC LIMIT :limit"); $query->bindValue(':limit', $config['mod']['noticeboard_dashboard'], PDO::PARAM_INT); $query->execute() or error(db_error($query)); $args['noticeboard'] = $query->fetchAll(PDO::FETCH_ASSOC); - if ($config['cache']['enabled']) - cache::set('noticeboard_preview', $args['noticeboard']); + $ctx->get(CacheDriver::class)->set('noticeboard_preview', $args['noticeboard']); } } - if (!$config['cache']['enabled'] || ($args['unread_pms'] = cache::get('pm_unreadcount_' . $mod['id'])) === false) { + if ($args['unread_pms'] = $ctx->get(CacheDriver::class)->get('pm_unreadcount_' . $mod['id']) === false) { $query = prepare('SELECT COUNT(*) FROM ``pms`` WHERE `to` = :id AND `unread` = 1'); $query->bindValue(':id', $mod['id']); $query->execute() or error(db_error($query)); $args['unread_pms'] = $query->fetchColumn(); - if ($config['cache']['enabled']) - cache::set('pm_unreadcount_' . $mod['id'], $args['unread_pms']); + $ctx->get(CacheDriver::class)->set('pm_unreadcount_' . $mod['id'], $args['unread_pms']); } $query = query('SELECT COUNT(*) FROM ``reports``') or error(db_error($query)); @@ -384,6 +382,8 @@ function mod_search(Context $ctx, $type, $search_query_escaped, $page_no = 1) { function mod_edit_board(Context $ctx, $boardName) { global $board, $config, $mod; + $cache = $ctx->get(CacheDriver::class); + if (!openBoard($boardName)) error($config['error']['noboard']); @@ -399,10 +399,8 @@ function mod_edit_board(Context $ctx, $boardName) { $query->bindValue(':uri', $board['uri']); $query->execute() or error(db_error($query)); - if ($config['cache']['enabled']) { - cache::delete('board_' . $board['uri']); - cache::delete('all_boards'); - } + $cache->delete('board_' . $board['uri']); + $cache->delete('all_boards'); modLog('Deleted board: ' . sprintf($config['board_abbreviation'], $board['uri']), false); @@ -467,10 +465,9 @@ function mod_edit_board(Context $ctx, $boardName) { modLog('Edited board information for ' . sprintf($config['board_abbreviation'], $board['uri']), false); } - if ($config['cache']['enabled']) { - cache::delete('board_' . $board['uri']); - cache::delete('all_boards'); - } + $cache->delete('board_' . $board['uri']); + $cache->delete('all_boards'); + Vichan\Functions\Theme\rebuild_themes('boards'); @@ -505,6 +502,8 @@ function mod_new_board(Context $ctx) { if (!preg_match('/^' . $config['board_regex'] . '$/u', $_POST['uri'])) error(sprintf($config['error']['invalidfield'], 'URI')); + $cache = $ctx->get(CacheDriver::class); + $bytes = 0; $chars = preg_split('//u', $_POST['uri'], -1, PREG_SPLIT_NO_EMPTY); foreach ($chars as $char) { @@ -544,8 +543,8 @@ function mod_new_board(Context $ctx) { query($query) or error(db_error()); - if ($config['cache']['enabled']) - cache::delete('all_boards'); + $cache = $ctx->get(CacheDriver::class); + $cache->delete('all_boards'); // Build the board buildIndex(); @@ -590,8 +589,8 @@ function mod_noticeboard(Context $ctx, $page_no = 1) { $query->bindValue(':body', $_POST['body']); $query->execute() or error(db_error($query)); - if ($config['cache']['enabled']) - cache::delete('noticeboard_preview'); + $cache = $ctx->get(CacheDriver::class); + $cache->delete('noticeboard_preview'); modLog('Posted a noticeboard entry'); @@ -631,7 +630,7 @@ function mod_noticeboard_delete(Context $ctx, $id) { $config = $ctx->get('config'); if (!hasPermission($config['mod']['noticeboard_delete'])) - error($config['error']['noaccess']); + error($config['error']['noaccess']); $query = prepare('DELETE FROM ``noticeboard`` WHERE `id` = :id'); $query->bindValue(':id', $id); @@ -639,8 +638,8 @@ function mod_noticeboard_delete(Context $ctx, $id) { modLog('Deleted a noticeboard entry'); - if ($config['cache']['enabled']) - cache::delete('noticeboard_preview'); + $cache = $ctx->get(CacheDriver::class); + $cache->delete('noticeboard_preview'); header('Location: ?/noticeboard', true, $config['redirect_http']); } @@ -706,7 +705,7 @@ function mod_news_delete(Context $ctx, $id) { $config = $ctx->get('config'); if (!hasPermission($config['mod']['news_delete'])) - error($config['error']['noaccess']); + error($config['error']['noaccess']); $query = prepare('DELETE FROM ``news`` WHERE `id` = :id'); $query->bindValue(':id', $id); @@ -843,7 +842,7 @@ function mod_view_board(Context $ctx, $boardName, $page_no = 1) { } $page['pages'] = getPages(true); - $page['pages'][$page_no-1]['selected'] = true; + $page['pages'][$page_no - 1]['selected'] = true; $page['btn'] = getPageButtons($page['pages'], true); $page['mod'] = true; $page['config'] = $config; @@ -1042,7 +1041,6 @@ function mod_edit_ban(Context $ctx, $ban_id) { Bans::delete($ban_id); header('Location: ?/', true, $config['redirect_http']); - } $args['token'] = make_secure_link_token('edit_ban/' . $ban_id); @@ -2235,10 +2233,9 @@ function mod_pm(Context $ctx, $id, $reply = false) { $query->bindValue(':id', $id); $query->execute() or error(db_error($query)); - if ($config['cache']['enabled']) { - cache::delete('pm_unread_' . $mod['id']); - cache::delete('pm_unreadcount_' . $mod['id']); - } + $cache = $ctx->get(CacheDriver::class); + $cache->delete('pm_unread_' . $mod['id']); + $cache->delete('pm_unreadcount_' . $mod['id']); header('Location: ?/', true, $config['redirect_http']); return; @@ -2249,10 +2246,9 @@ function mod_pm(Context $ctx, $id, $reply = false) { $query->bindValue(':id', $id); $query->execute() or error(db_error($query)); - if ($config['cache']['enabled']) { - cache::delete('pm_unread_' . $mod['id']); - cache::delete('pm_unreadcount_' . $mod['id']); - } + $cache = $ctx->get(CacheDriver::class); + $cache->delete('pm_unread_' . $mod['id']); + $cache->delete('pm_unreadcount_' . $mod['id']); modLog('Read a PM'); } @@ -2339,10 +2335,10 @@ function mod_new_pm(Context $ctx, $username) { $query->bindValue(':time', time()); $query->execute() or error(db_error($query)); - if ($config['cache']['enabled']) { - cache::delete('pm_unread_' . $id); - cache::delete('pm_unreadcount_' . $id); - } + $cache = $ctx->get(CacheDriver::class); + + $cache->delete('pm_unread_' . $id); + $cache->delete('pm_unreadcount_' . $id); modLog('Sent a PM to ' . utf8tohtml($username)); @@ -2368,6 +2364,8 @@ function mod_rebuild(Context $ctx) { if (!hasPermission($config['mod']['rebuild'])) error($config['error']['noaccess']); + $cache = $ctx->get(CacheDriver::class); + if (isset($_POST['rebuild'])) { @set_time_limit($config['mod']['rebuild_timelimit']); @@ -2378,7 +2376,7 @@ function mod_rebuild(Context $ctx) { if (isset($_POST['rebuild_cache'])) { if ($config['cache']['enabled']) { $log[] = 'Flushing cache'; - Cache::flush(); + $cache->flush(); } $log[] = 'Clearing template cache'; @@ -2840,6 +2838,8 @@ function mod_theme_configure(Context $ctx, $theme_name) { error($config['error']['invalidtheme']); } + $cache = $ctx->get(CacheDriver::class); + if (isset($_POST['install'])) { // Check if everything is submitted foreach ($theme['config'] as &$conf) { @@ -2868,8 +2868,8 @@ function mod_theme_configure(Context $ctx, $theme_name) { $query->execute() or error(db_error($query)); // Clean cache - Cache::delete("themes"); - Cache::delete("theme_settings_".$theme_name); + $cache->delete("themes"); + $cache->delete("theme_settings_$theme_name"); $result = true; $message = false; @@ -2928,13 +2928,15 @@ function mod_theme_uninstall(Context $ctx, $theme_name) { if (!hasPermission($config['mod']['themes'])) error($config['error']['noaccess']); + $cache = $ctx->get(CacheDriver::class); + $query = prepare("DELETE FROM ``theme_settings`` WHERE `theme` = :theme"); $query->bindValue(':theme', $theme_name); $query->execute() or error(db_error($query)); // Clean cache - Cache::delete("themes"); - Cache::delete("theme_settings_".$theme_name); + $cache->delete("themes"); + $cache->delete("theme_settings_$theme_name"); header('Location: ?/themes', true, $config['redirect_http']); } @@ -2959,7 +2961,7 @@ function mod_theme_rebuild(Context $ctx, $theme_name) { } // This needs to be done for `secure` CSRF prevention compatibility, otherwise the $board will be read in as the token if editing global pages. -function delete_page_base($page = '', $board = false) { +function delete_page_base(Context $ctx, $page = '', $board = false) { global $config, $mod; if (empty($board)) From 003e8f6d3b0a645ccaa4b131fee31e7dc8b91278 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Fri, 20 Sep 2024 22:41:51 +0200 Subject: [PATCH 10/11] maintenance.php: delete expired filesystem cache --- tools/maintenance.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tools/maintenance.php b/tools/maintenance.php index 33c0a4d4..a869e2fa 100644 --- a/tools/maintenance.php +++ b/tools/maintenance.php @@ -21,5 +21,20 @@ echo "Deleted $deleted_count expired antispam in $delta seconds!\n"; $time_tot = $delta; $deleted_tot = $deleted_count; +if ($config['cache']['enabled'] === 'fs') { + $fs_cache = new Vichan\Data\Driver\FsCacheDriver( + $config['cache']['prefix'], + "tmp/cache/{$config['cache']['prefix']}", + '.lock', + false + ); + $start = microtime(true); + $fs_cache->collect(); + $delta = microtime(true) - $start; + echo "Deleted $deleted_count expired filesystem cache items in $delta seconds!\n"; + $time_tot = $delta; + $deleted_tot = $deleted_count; +} + $time_tot = number_format((float)$time_tot, 4, '.', ''); modLog("Deleted $deleted_tot expired entries in {$time_tot}s with maintenance tool"); From 115f28807a614b816a6f519a1bfa5160ff95cc38 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Sat, 5 Oct 2024 17:31:41 +0200 Subject: [PATCH 11/11] FsCacheDriver.php: collect expired cache items before operating on the cache --- inc/Data/Driver/FsCachedriver.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/inc/Data/Driver/FsCachedriver.php b/inc/Data/Driver/FsCachedriver.php index f7f0e028..b543cfa6 100644 --- a/inc/Data/Driver/FsCachedriver.php +++ b/inc/Data/Driver/FsCachedriver.php @@ -30,7 +30,10 @@ class FsCacheDriver implements CacheDriver { } private function collectImpl(): int { - // A read lock is ok, since it's alright if we delete expired items from under the feet of other processes. + /* + * A read lock is ok, since it's alright if we delete expired items from under the feet of other processes, and + * no other process add new cache items or refresh existing ones. + */ $files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT); $count = 0; foreach ($files as $file) { @@ -84,6 +87,9 @@ class FsCacheDriver implements CacheDriver { $this->sharedLockCache(); + // Collect expired items first so if the target key is expired we shortcut to failure in the next lines. + $this->maybeCollect(); + $fd = \fopen($this->base_path . $key, 'r'); if ($fd === false) { $this->unlockCache(); @@ -92,7 +98,6 @@ class FsCacheDriver implements CacheDriver { $data = \stream_get_contents($fd); \fclose($fd); - $this->maybeCollect(); $this->unlockCache(); $wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR); @@ -114,8 +119,8 @@ class FsCacheDriver implements CacheDriver { $data = \json_encode($wrapped); $this->exclusiveLockCache(); - \file_put_contents($this->base_path . $key, $data); $this->maybeCollect(); + \file_put_contents($this->base_path . $key, $data); $this->unlockCache(); }