aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Memcache
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Memcache')
-rw-r--r--lib/private/Memcache/APCu.php124
-rw-r--r--lib/private/Memcache/ArrayCache.php142
-rw-r--r--lib/private/Memcache/CADTrait.php56
-rw-r--r--lib/private/Memcache/CASTrait.php42
-rw-r--r--lib/private/Memcache/Cache.php86
-rw-r--r--lib/private/Memcache/Factory.php220
-rw-r--r--lib/private/Memcache/LoggerWrapperCache.php179
-rw-r--r--lib/private/Memcache/Memcached.php172
-rw-r--r--lib/private/Memcache/NullCache.php59
-rw-r--r--lib/private/Memcache/ProfilerWrapperCache.php225
-rw-r--r--lib/private/Memcache/Redis.php222
-rw-r--r--lib/private/Memcache/WithLocalCache.php58
12 files changed, 1585 insertions, 0 deletions
diff --git a/lib/private/Memcache/APCu.php b/lib/private/Memcache/APCu.php
new file mode 100644
index 00000000000..937f8a863ab
--- /dev/null
+++ b/lib/private/Memcache/APCu.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Memcache;
+
+use bantu\IniGetWrapper\IniGetWrapper;
+use OCP\IMemcache;
+
+class APCu extends Cache implements IMemcache {
+ use CASTrait {
+ cas as casEmulated;
+ }
+
+ use CADTrait;
+
+ public function get($key) {
+ $result = apcu_fetch($this->getPrefix() . $key, $success);
+ if (!$success) {
+ return null;
+ }
+ return $result;
+ }
+
+ public function set($key, $value, $ttl = 0) {
+ if ($ttl === 0) {
+ $ttl = self::DEFAULT_TTL;
+ }
+ return apcu_store($this->getPrefix() . $key, $value, $ttl);
+ }
+
+ public function hasKey($key) {
+ return apcu_exists($this->getPrefix() . $key);
+ }
+
+ public function remove($key) {
+ return apcu_delete($this->getPrefix() . $key);
+ }
+
+ public function clear($prefix = '') {
+ $ns = $this->getPrefix() . $prefix;
+ $ns = preg_quote($ns, '/');
+ if (class_exists('\APCIterator')) {
+ $iter = new \APCIterator('user', '/^' . $ns . '/', APC_ITER_KEY);
+ } else {
+ $iter = new \APCUIterator('/^' . $ns . '/', APC_ITER_KEY);
+ }
+ return apcu_delete($iter);
+ }
+
+ /**
+ * Set a value in the cache if it's not already stored
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param int $ttl Time To Live in seconds. Defaults to 60*60*24
+ * @return bool
+ */
+ public function add($key, $value, $ttl = 0) {
+ if ($ttl === 0) {
+ $ttl = self::DEFAULT_TTL;
+ }
+ return apcu_add($this->getPrefix() . $key, $value, $ttl);
+ }
+
+ /**
+ * Increase a stored number
+ *
+ * @param string $key
+ * @param int $step
+ * @return int | bool
+ */
+ public function inc($key, $step = 1) {
+ $success = null;
+ return apcu_inc($this->getPrefix() . $key, $step, $success, self::DEFAULT_TTL);
+ }
+
+ /**
+ * Decrease a stored number
+ *
+ * @param string $key
+ * @param int $step
+ * @return int | bool
+ */
+ public function dec($key, $step = 1) {
+ return apcu_exists($this->getPrefix() . $key)
+ ? apcu_dec($this->getPrefix() . $key, $step)
+ : false;
+ }
+
+ /**
+ * Compare and set
+ *
+ * @param string $key
+ * @param mixed $old
+ * @param mixed $new
+ * @return bool
+ */
+ public function cas($key, $old, $new) {
+ // apc only does cas for ints
+ if (is_int($old) and is_int($new)) {
+ return apcu_cas($this->getPrefix() . $key, $old, $new);
+ } else {
+ return $this->casEmulated($key, $old, $new);
+ }
+ }
+
+ public static function isAvailable(): bool {
+ if (!extension_loaded('apcu')) {
+ return false;
+ } elseif (!\OC::$server->get(IniGetWrapper::class)->getBool('apc.enabled')) {
+ return false;
+ } elseif (!\OC::$server->get(IniGetWrapper::class)->getBool('apc.enable_cli') && \OC::$CLI) {
+ return false;
+ } elseif (version_compare(phpversion('apcu') ?: '0.0.0', '5.1.0') === -1) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/lib/private/Memcache/ArrayCache.php b/lib/private/Memcache/ArrayCache.php
new file mode 100644
index 00000000000..9b3540b771f
--- /dev/null
+++ b/lib/private/Memcache/ArrayCache.php
@@ -0,0 +1,142 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Memcache;
+
+use OCP\IMemcache;
+
+class ArrayCache extends Cache implements IMemcache {
+ /** @var array Array with the cached data */
+ protected $cachedData = [];
+
+ use CADTrait;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function get($key) {
+ if ($this->hasKey($key)) {
+ return $this->cachedData[$key];
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function set($key, $value, $ttl = 0) {
+ $this->cachedData[$key] = $value;
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function hasKey($key) {
+ return isset($this->cachedData[$key]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function remove($key) {
+ unset($this->cachedData[$key]);
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function clear($prefix = '') {
+ if ($prefix === '') {
+ $this->cachedData = [];
+ return true;
+ }
+
+ foreach ($this->cachedData as $key => $value) {
+ if (str_starts_with($key, $prefix)) {
+ $this->remove($key);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Set a value in the cache if it's not already stored
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param int $ttl Time To Live in seconds. Defaults to 60*60*24
+ * @return bool
+ */
+ public function add($key, $value, $ttl = 0) {
+ // since this cache is not shared race conditions aren't an issue
+ if ($this->hasKey($key)) {
+ return false;
+ } else {
+ return $this->set($key, $value, $ttl);
+ }
+ }
+
+ /**
+ * Increase a stored number
+ *
+ * @param string $key
+ * @param int $step
+ * @return int | bool
+ */
+ public function inc($key, $step = 1) {
+ $oldValue = $this->get($key);
+ if (is_int($oldValue)) {
+ $this->set($key, $oldValue + $step);
+ return $oldValue + $step;
+ } else {
+ $success = $this->add($key, $step);
+ return $success ? $step : false;
+ }
+ }
+
+ /**
+ * Decrease a stored number
+ *
+ * @param string $key
+ * @param int $step
+ * @return int | bool
+ */
+ public function dec($key, $step = 1) {
+ $oldValue = $this->get($key);
+ if (is_int($oldValue)) {
+ $this->set($key, $oldValue - $step);
+ return $oldValue - $step;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Compare and set
+ *
+ * @param string $key
+ * @param mixed $old
+ * @param mixed $new
+ * @return bool
+ */
+ public function cas($key, $old, $new) {
+ if ($this->get($key) === $old) {
+ return $this->set($key, $new);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public static function isAvailable(): bool {
+ return true;
+ }
+}
diff --git a/lib/private/Memcache/CADTrait.php b/lib/private/Memcache/CADTrait.php
new file mode 100644
index 00000000000..d0f6611c4f3
--- /dev/null
+++ b/lib/private/Memcache/CADTrait.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Memcache;
+
+trait CADTrait {
+ abstract public function get($key);
+
+ abstract public function remove($key);
+
+ abstract public function add($key, $value, $ttl = 0);
+
+ /**
+ * Compare and delete
+ *
+ * @param string $key
+ * @param mixed $old
+ * @return bool
+ */
+ public function cad($key, $old) {
+ //no native cas, emulate with locking
+ if ($this->add($key . '_lock', true)) {
+ if ($this->get($key) === $old) {
+ $this->remove($key);
+ $this->remove($key . '_lock');
+ return true;
+ } else {
+ $this->remove($key . '_lock');
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ public function ncad(string $key, mixed $old): bool {
+ //no native cad, emulate with locking
+ if ($this->add($key . '_lock', true)) {
+ $value = $this->get($key);
+ if ($value !== null && $value !== $old) {
+ $this->remove($key);
+ $this->remove($key . '_lock');
+ return true;
+ } else {
+ $this->remove($key . '_lock');
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/lib/private/Memcache/CASTrait.php b/lib/private/Memcache/CASTrait.php
new file mode 100644
index 00000000000..8c2d2a46b19
--- /dev/null
+++ b/lib/private/Memcache/CASTrait.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Memcache;
+
+trait CASTrait {
+ abstract public function get($key);
+
+ abstract public function set($key, $value, $ttl = 0);
+
+ abstract public function remove($key);
+
+ abstract public function add($key, $value, $ttl = 0);
+
+ /**
+ * Compare and set
+ *
+ * @param string $key
+ * @param mixed $old
+ * @param mixed $new
+ * @return bool
+ */
+ public function cas($key, $old, $new) {
+ //no native cas, emulate with locking
+ if ($this->add($key . '_lock', true)) {
+ if ($this->get($key) === $old) {
+ $this->set($key, $new);
+ $this->remove($key . '_lock');
+ return true;
+ } else {
+ $this->remove($key . '_lock');
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/lib/private/Memcache/Cache.php b/lib/private/Memcache/Cache.php
new file mode 100644
index 00000000000..774769b25fe
--- /dev/null
+++ b/lib/private/Memcache/Cache.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Memcache;
+
+/**
+ * @template-implements \ArrayAccess<string,mixed>
+ */
+abstract class Cache implements \ArrayAccess, \OCP\ICache {
+ /**
+ * @var string $prefix
+ */
+ protected $prefix;
+
+ /**
+ * @param string $prefix
+ */
+ public function __construct($prefix = '') {
+ $this->prefix = $prefix;
+ }
+
+ /**
+ * @return string Prefix used for caching purposes
+ */
+ public function getPrefix() {
+ return $this->prefix;
+ }
+
+ /**
+ * @param string $key
+ * @return mixed
+ */
+ abstract public function get($key);
+
+ /**
+ * @param string $key
+ * @param mixed $value
+ * @param int $ttl
+ * @return mixed
+ */
+ abstract public function set($key, $value, $ttl = 0);
+
+ /**
+ * @param string $key
+ * @return mixed
+ */
+ abstract public function hasKey($key);
+
+ /**
+ * @param string $key
+ * @return mixed
+ */
+ abstract public function remove($key);
+
+ /**
+ * @param string $prefix
+ * @return mixed
+ */
+ abstract public function clear($prefix = '');
+
+ //implement the ArrayAccess interface
+
+ public function offsetExists($offset): bool {
+ return $this->hasKey($offset);
+ }
+
+ public function offsetSet($offset, $value): void {
+ $this->set($offset, $value);
+ }
+
+ /**
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($offset) {
+ return $this->get($offset);
+ }
+
+ public function offsetUnset($offset): void {
+ $this->remove($offset);
+ }
+}
diff --git a/lib/private/Memcache/Factory.php b/lib/private/Memcache/Factory.php
new file mode 100644
index 00000000000..b54189937fc
--- /dev/null
+++ b/lib/private/Memcache/Factory.php
@@ -0,0 +1,220 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Memcache;
+
+use Closure;
+use OCP\Cache\CappedMemoryCache;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IMemcache;
+use OCP\Profiler\IProfiler;
+use Psr\Log\LoggerInterface;
+
+class Factory implements ICacheFactory {
+ public const NULL_CACHE = NullCache::class;
+
+ private ?string $globalPrefix = null;
+
+ private LoggerInterface $logger;
+
+ /**
+ * @var ?class-string<ICache> $localCacheClass
+ */
+ private ?string $localCacheClass;
+
+ /**
+ * @var ?class-string<ICache> $distributedCacheClass
+ */
+ private ?string $distributedCacheClass;
+
+ /**
+ * @var ?class-string<IMemcache> $lockingCacheClass
+ */
+ private ?string $lockingCacheClass;
+
+ private string $logFile;
+
+ private IProfiler $profiler;
+
+ /**
+ * @param Closure $globalPrefixClosure
+ * @param LoggerInterface $logger
+ * @param ?class-string<ICache> $localCacheClass
+ * @param ?class-string<ICache> $distributedCacheClass
+ * @param ?class-string<IMemcache> $lockingCacheClass
+ * @param string $logFile
+ */
+ public function __construct(
+ private Closure $globalPrefixClosure,
+ LoggerInterface $logger,
+ IProfiler $profiler,
+ ?string $localCacheClass = null,
+ ?string $distributedCacheClass = null,
+ ?string $lockingCacheClass = null,
+ string $logFile = '',
+ ) {
+ $this->logFile = $logFile;
+
+ if (!$localCacheClass) {
+ $localCacheClass = self::NULL_CACHE;
+ }
+ $localCacheClass = ltrim($localCacheClass, '\\');
+ if (!$distributedCacheClass) {
+ $distributedCacheClass = $localCacheClass;
+ }
+
+ $distributedCacheClass = ltrim($distributedCacheClass, '\\');
+
+ $missingCacheMessage = 'Memcache {class} not available for {use} cache';
+ $missingCacheHint = 'Is the matching PHP module installed and enabled?';
+ if (!class_exists($localCacheClass) || !$localCacheClass::isAvailable()) {
+ if (\OC::$CLI && !defined('PHPUNIT_RUN') && $localCacheClass === APCu::class) {
+ // CLI should not fail if APCu is not available but fallback to NullCache.
+ // This can be the case if APCu is used without apc.enable_cli=1.
+ // APCu however cannot be shared between PHP instances (CLI and web) anyway.
+ $localCacheClass = self::NULL_CACHE;
+ } else {
+ throw new \OCP\HintException(strtr($missingCacheMessage, [
+ '{class}' => $localCacheClass, '{use}' => 'local'
+ ]), $missingCacheHint);
+ }
+ }
+ if (!class_exists($distributedCacheClass) || !$distributedCacheClass::isAvailable()) {
+ if (\OC::$CLI && !defined('PHPUNIT_RUN') && $distributedCacheClass === APCu::class) {
+ // CLI should not fail if APCu is not available but fallback to NullCache.
+ // This can be the case if APCu is used without apc.enable_cli=1.
+ // APCu however cannot be shared between Nextcloud (PHP) instances anyway.
+ $distributedCacheClass = self::NULL_CACHE;
+ } else {
+ throw new \OCP\HintException(strtr($missingCacheMessage, [
+ '{class}' => $distributedCacheClass, '{use}' => 'distributed'
+ ]), $missingCacheHint);
+ }
+ }
+ if (!($lockingCacheClass && class_exists($lockingCacheClass) && $lockingCacheClass::isAvailable())) {
+ // don't fall back since the fallback might not be suitable for storing lock
+ $lockingCacheClass = self::NULL_CACHE;
+ }
+ $lockingCacheClass = ltrim($lockingCacheClass, '\\');
+
+ $this->localCacheClass = $localCacheClass;
+ $this->distributedCacheClass = $distributedCacheClass;
+ $this->lockingCacheClass = $lockingCacheClass;
+ $this->profiler = $profiler;
+ }
+
+ private function getGlobalPrefix(): ?string {
+ if (is_null($this->globalPrefix)) {
+ $this->globalPrefix = ($this->globalPrefixClosure)();
+ }
+ return $this->globalPrefix;
+ }
+
+ /**
+ * create a cache instance for storing locks
+ *
+ * @param string $prefix
+ * @return IMemcache
+ */
+ public function createLocking(string $prefix = ''): IMemcache {
+ $globalPrefix = $this->getGlobalPrefix();
+ if (is_null($globalPrefix)) {
+ return new ArrayCache($prefix);
+ }
+
+ assert($this->lockingCacheClass !== null);
+ $cache = new $this->lockingCacheClass($globalPrefix . '/' . $prefix);
+ if ($this->lockingCacheClass === Redis::class && $this->profiler->isEnabled()) {
+ // We only support the profiler with Redis
+ $cache = new ProfilerWrapperCache($cache, 'Locking');
+ $this->profiler->add($cache);
+ }
+
+ if ($this->lockingCacheClass === Redis::class
+ && $this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
+ $cache = new LoggerWrapperCache($cache, $this->logFile);
+ }
+ return $cache;
+ }
+
+ /**
+ * create a distributed cache instance
+ *
+ * @param string $prefix
+ * @return ICache
+ */
+ public function createDistributed(string $prefix = ''): ICache {
+ $globalPrefix = $this->getGlobalPrefix();
+ if (is_null($globalPrefix)) {
+ return new ArrayCache($prefix);
+ }
+
+ assert($this->distributedCacheClass !== null);
+ $cache = new $this->distributedCacheClass($globalPrefix . '/' . $prefix);
+ if ($this->distributedCacheClass === Redis::class && $this->profiler->isEnabled()) {
+ // We only support the profiler with Redis
+ $cache = new ProfilerWrapperCache($cache, 'Distributed');
+ $this->profiler->add($cache);
+ }
+
+ if ($this->distributedCacheClass === Redis::class && $this->logFile !== ''
+ && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
+ $cache = new LoggerWrapperCache($cache, $this->logFile);
+ }
+ return $cache;
+ }
+
+ /**
+ * create a local cache instance
+ *
+ * @param string $prefix
+ * @return ICache
+ */
+ public function createLocal(string $prefix = ''): ICache {
+ $globalPrefix = $this->getGlobalPrefix();
+ if (is_null($globalPrefix)) {
+ return new ArrayCache($prefix);
+ }
+
+ assert($this->localCacheClass !== null);
+ $cache = new $this->localCacheClass($globalPrefix . '/' . $prefix);
+ if ($this->localCacheClass === Redis::class && $this->profiler->isEnabled()) {
+ // We only support the profiler with Redis
+ $cache = new ProfilerWrapperCache($cache, 'Local');
+ $this->profiler->add($cache);
+ }
+
+ if ($this->localCacheClass === Redis::class && $this->logFile !== ''
+ && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
+ $cache = new LoggerWrapperCache($cache, $this->logFile);
+ }
+ return $cache;
+ }
+
+ /**
+ * check memcache availability
+ *
+ * @return bool
+ */
+ public function isAvailable(): bool {
+ return $this->distributedCacheClass !== self::NULL_CACHE;
+ }
+
+ public function createInMemory(int $capacity = 512): ICache {
+ return new CappedMemoryCache($capacity);
+ }
+
+ /**
+ * Check if a local memory cache backend is available
+ *
+ * @return bool
+ */
+ public function isLocalCacheAvailable(): bool {
+ return $this->localCacheClass !== self::NULL_CACHE;
+ }
+}
diff --git a/lib/private/Memcache/LoggerWrapperCache.php b/lib/private/Memcache/LoggerWrapperCache.php
new file mode 100644
index 00000000000..c2a06731910
--- /dev/null
+++ b/lib/private/Memcache/LoggerWrapperCache.php
@@ -0,0 +1,179 @@
+<?php
+
+declare(strict_types = 1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Memcache;
+
+use OCP\IMemcacheTTL;
+
+/**
+ * Cache wrapper that logs the cache operation in a log file
+ */
+class LoggerWrapperCache extends Cache implements IMemcacheTTL {
+ /** @var Redis */
+ protected $wrappedCache;
+
+ /** @var string $logFile */
+ private $logFile;
+
+ /** @var string $prefix */
+ protected $prefix;
+
+ public function __construct(Redis $wrappedCache, string $logFile) {
+ parent::__construct($wrappedCache->getPrefix());
+ $this->wrappedCache = $wrappedCache;
+ $this->logFile = $logFile;
+ }
+
+ /**
+ * @return string Prefix used for caching purposes
+ */
+ public function getPrefix() {
+ return $this->prefix;
+ }
+
+ protected function getNameSpace() {
+ return $this->prefix;
+ }
+
+ /** @inheritDoc */
+ public function get($key) {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::get::' . $key . "\n",
+ FILE_APPEND
+ );
+ return $this->wrappedCache->get($key);
+ }
+
+ /** @inheritDoc */
+ public function set($key, $value, $ttl = 0) {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::set::' . $key . '::' . $ttl . '::' . json_encode($value) . "\n",
+ FILE_APPEND
+ );
+
+ return $this->wrappedCache->set($key, $value, $ttl);
+ }
+
+ /** @inheritDoc */
+ public function hasKey($key) {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::hasKey::' . $key . "\n",
+ FILE_APPEND
+ );
+
+ return $this->wrappedCache->hasKey($key);
+ }
+
+ /** @inheritDoc */
+ public function remove($key) {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::remove::' . $key . "\n",
+ FILE_APPEND
+ );
+
+ return $this->wrappedCache->remove($key);
+ }
+
+ /** @inheritDoc */
+ public function clear($prefix = '') {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::clear::' . $prefix . "\n",
+ FILE_APPEND
+ );
+
+ return $this->wrappedCache->clear($prefix);
+ }
+
+ /** @inheritDoc */
+ public function add($key, $value, $ttl = 0) {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::add::' . $key . '::' . $value . "\n",
+ FILE_APPEND
+ );
+
+ return $this->wrappedCache->add($key, $value, $ttl);
+ }
+
+ /** @inheritDoc */
+ public function inc($key, $step = 1) {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::inc::' . $key . "\n",
+ FILE_APPEND
+ );
+
+ return $this->wrappedCache->inc($key, $step);
+ }
+
+ /** @inheritDoc */
+ public function dec($key, $step = 1) {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::dec::' . $key . "\n",
+ FILE_APPEND
+ );
+
+ return $this->wrappedCache->dec($key, $step);
+ }
+
+ /** @inheritDoc */
+ public function cas($key, $old, $new) {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::cas::' . $key . "\n",
+ FILE_APPEND
+ );
+
+ return $this->wrappedCache->cas($key, $old, $new);
+ }
+
+ /** @inheritDoc */
+ public function cad($key, $old) {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::cad::' . $key . "\n",
+ FILE_APPEND
+ );
+
+ return $this->wrappedCache->cad($key, $old);
+ }
+
+ /** @inheritDoc */
+ public function ncad(string $key, mixed $old): bool {
+ file_put_contents(
+ $this->logFile,
+ $this->getNameSpace() . '::ncad::' . $key . "\n",
+ FILE_APPEND
+ );
+
+ return $this->wrappedCache->cad($key, $old);
+ }
+
+ /** @inheritDoc */
+ public function setTTL(string $key, int $ttl) {
+ $this->wrappedCache->setTTL($key, $ttl);
+ }
+
+ public function getTTL(string $key): int|false {
+ return $this->wrappedCache->getTTL($key);
+ }
+
+ public function compareSetTTL(string $key, mixed $value, int $ttl): bool {
+ return $this->wrappedCache->compareSetTTL($key, $value, $ttl);
+ }
+
+ public static function isAvailable(): bool {
+ return true;
+ }
+}
diff --git a/lib/private/Memcache/Memcached.php b/lib/private/Memcache/Memcached.php
new file mode 100644
index 00000000000..d8b624a978a
--- /dev/null
+++ b/lib/private/Memcache/Memcached.php
@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Memcache;
+
+use OCP\HintException;
+use OCP\IMemcache;
+
+class Memcached extends Cache implements IMemcache {
+ use CASTrait;
+
+ /**
+ * @var \Memcached $cache
+ */
+ private static $cache = null;
+
+ use CADTrait;
+
+ public function __construct($prefix = '') {
+ parent::__construct($prefix);
+ if (is_null(self::$cache)) {
+ self::$cache = new \Memcached();
+
+ $defaultOptions = [
+ \Memcached::OPT_CONNECT_TIMEOUT => 50,
+ \Memcached::OPT_RETRY_TIMEOUT => 50,
+ \Memcached::OPT_SEND_TIMEOUT => 50,
+ \Memcached::OPT_RECV_TIMEOUT => 50,
+ \Memcached::OPT_POLL_TIMEOUT => 50,
+
+ // Enable compression
+ \Memcached::OPT_COMPRESSION => true,
+
+ // Turn on consistent hashing
+ \Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
+
+ // Enable Binary Protocol
+ \Memcached::OPT_BINARY_PROTOCOL => true,
+ ];
+ /**
+ * By default enable igbinary serializer if available
+ *
+ * Psalm checks depend on if igbinary is installed or not with memcached
+ * @psalm-suppress RedundantCondition
+ * @psalm-suppress TypeDoesNotContainType
+ */
+ if (\Memcached::HAVE_IGBINARY) {
+ $defaultOptions[\Memcached::OPT_SERIALIZER]
+ = \Memcached::SERIALIZER_IGBINARY;
+ }
+ $options = \OC::$server->getConfig()->getSystemValue('memcached_options', []);
+ if (is_array($options)) {
+ $options = $options + $defaultOptions;
+ self::$cache->setOptions($options);
+ } else {
+ throw new HintException("Expected 'memcached_options' config to be an array, got $options");
+ }
+
+ $servers = \OC::$server->getSystemConfig()->getValue('memcached_servers');
+ if (!$servers) {
+ $server = \OC::$server->getSystemConfig()->getValue('memcached_server');
+ if ($server) {
+ $servers = [$server];
+ } else {
+ $servers = [['localhost', 11211]];
+ }
+ }
+ self::$cache->addServers($servers);
+ }
+ }
+
+ /**
+ * entries in XCache gets namespaced to prevent collisions between owncloud instances and users
+ */
+ protected function getNameSpace() {
+ return $this->prefix;
+ }
+
+ public function get($key) {
+ $result = self::$cache->get($this->getNameSpace() . $key);
+ if ($result === false and self::$cache->getResultCode() == \Memcached::RES_NOTFOUND) {
+ return null;
+ } else {
+ return $result;
+ }
+ }
+
+ public function set($key, $value, $ttl = 0) {
+ if ($ttl > 0) {
+ $result = self::$cache->set($this->getNameSpace() . $key, $value, $ttl);
+ } else {
+ $result = self::$cache->set($this->getNameSpace() . $key, $value);
+ }
+ return $result || $this->isSuccess();
+ }
+
+ public function hasKey($key) {
+ self::$cache->get($this->getNameSpace() . $key);
+ return self::$cache->getResultCode() === \Memcached::RES_SUCCESS;
+ }
+
+ public function remove($key) {
+ $result = self::$cache->delete($this->getNameSpace() . $key);
+ return $result || $this->isSuccess() || self::$cache->getResultCode() === \Memcached::RES_NOTFOUND;
+ }
+
+ public function clear($prefix = '') {
+ // Newer Memcached doesn't like getAllKeys(), flush everything
+ self::$cache->flush();
+ return true;
+ }
+
+ /**
+ * Set a value in the cache if it's not already stored
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param int $ttl Time To Live in seconds. Defaults to 60*60*24
+ * @return bool
+ */
+ public function add($key, $value, $ttl = 0) {
+ $result = self::$cache->add($this->getPrefix() . $key, $value, $ttl);
+ return $result || $this->isSuccess();
+ }
+
+ /**
+ * Increase a stored number
+ *
+ * @param string $key
+ * @param int $step
+ * @return int | bool
+ */
+ public function inc($key, $step = 1) {
+ $this->add($key, 0);
+ $result = self::$cache->increment($this->getPrefix() . $key, $step);
+
+ if (self::$cache->getResultCode() !== \Memcached::RES_SUCCESS) {
+ return false;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Decrease a stored number
+ *
+ * @param string $key
+ * @param int $step
+ * @return int | bool
+ */
+ public function dec($key, $step = 1) {
+ $result = self::$cache->decrement($this->getPrefix() . $key, $step);
+
+ if (self::$cache->getResultCode() !== \Memcached::RES_SUCCESS) {
+ return false;
+ }
+
+ return $result;
+ }
+
+ public static function isAvailable(): bool {
+ return extension_loaded('memcached');
+ }
+
+ private function isSuccess(): bool {
+ return self::$cache->getResultCode() === \Memcached::RES_SUCCESS;
+ }
+}
diff --git a/lib/private/Memcache/NullCache.php b/lib/private/Memcache/NullCache.php
new file mode 100644
index 00000000000..eac1e6ddadc
--- /dev/null
+++ b/lib/private/Memcache/NullCache.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Memcache;
+
+class NullCache extends Cache implements \OCP\IMemcache {
+ public function get($key) {
+ return null;
+ }
+
+ public function set($key, $value, $ttl = 0) {
+ return true;
+ }
+
+ public function hasKey($key) {
+ return false;
+ }
+
+ public function remove($key) {
+ return true;
+ }
+
+ public function add($key, $value, $ttl = 0) {
+ return true;
+ }
+
+ public function inc($key, $step = 1) {
+ return true;
+ }
+
+ public function dec($key, $step = 1) {
+ return true;
+ }
+
+ public function cas($key, $old, $new) {
+ return true;
+ }
+
+ public function cad($key, $old) {
+ return true;
+ }
+
+ public function ncad(string $key, mixed $old): bool {
+ return true;
+ }
+
+
+ public function clear($prefix = '') {
+ return true;
+ }
+
+ public static function isAvailable(): bool {
+ return true;
+ }
+}
diff --git a/lib/private/Memcache/ProfilerWrapperCache.php b/lib/private/Memcache/ProfilerWrapperCache.php
new file mode 100644
index 00000000000..97d9d828a32
--- /dev/null
+++ b/lib/private/Memcache/ProfilerWrapperCache.php
@@ -0,0 +1,225 @@
+<?php
+
+declare(strict_types = 1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Memcache;
+
+use OC\AppFramework\Http\Request;
+use OCP\AppFramework\Http\Response;
+use OCP\DataCollector\AbstractDataCollector;
+use OCP\IMemcacheTTL;
+
+/**
+ * Cache wrapper that logs profiling information
+ * @template-implements \ArrayAccess<string,mixed>
+ */
+class ProfilerWrapperCache extends AbstractDataCollector implements IMemcacheTTL, \ArrayAccess {
+ /** @var Redis $wrappedCache */
+ protected $wrappedCache;
+
+ /** @var string $prefix */
+ protected $prefix;
+
+ /** @var string $type */
+ private $type;
+
+ public function __construct(Redis $wrappedCache, string $type) {
+ $this->prefix = $wrappedCache->getPrefix();
+ $this->wrappedCache = $wrappedCache;
+ $this->type = $type;
+ $this->data['queries'] = [];
+ $this->data['cacheHit'] = 0;
+ $this->data['cacheMiss'] = 0;
+ }
+
+ public function getPrefix(): string {
+ return $this->prefix;
+ }
+
+ /** @inheritDoc */
+ public function get($key) {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->get($key);
+ if ($ret === null) {
+ $this->data['cacheMiss']++;
+ } else {
+ $this->data['cacheHit']++;
+ }
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::get::' . $key,
+ 'hit' => $ret !== null,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
+ public function set($key, $value, $ttl = 0) {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->set($key, $value, $ttl);
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::set::' . $key,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
+ public function hasKey($key) {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->hasKey($key);
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::hasKey::' . $key,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
+ public function remove($key) {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->remove($key);
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::remove::' . $key,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
+ public function clear($prefix = '') {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->clear($prefix);
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::clear::' . $prefix,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
+ public function add($key, $value, $ttl = 0) {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->add($key, $value, $ttl);
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::add::' . $key,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
+ public function inc($key, $step = 1) {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->inc($key, $step);
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::inc::' . $key,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
+ public function dec($key, $step = 1) {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->dec($key, $step);
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::dev::' . $key,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
+ public function cas($key, $old, $new) {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->cas($key, $old, $new);
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::cas::' . $key,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
+ public function cad($key, $old) {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->cad($key, $old);
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::cad::' . $key,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
+ public function ncad(string $key, mixed $old): bool {
+ $start = microtime(true);
+ $ret = $this->wrappedCache->ncad($key, $old);
+ $this->data['queries'][] = [
+ 'start' => $start,
+ 'end' => microtime(true),
+ 'op' => $this->getPrefix() . '::ncad::' . $key,
+ ];
+ return $ret;
+ }
+
+ /** @inheritDoc */
+ public function setTTL(string $key, int $ttl) {
+ $this->wrappedCache->setTTL($key, $ttl);
+ }
+
+ public function getTTL(string $key): int|false {
+ return $this->wrappedCache->getTTL($key);
+ }
+
+ public function compareSetTTL(string $key, mixed $value, int $ttl): bool {
+ return $this->wrappedCache->compareSetTTL($key, $value, $ttl);
+ }
+
+ public function offsetExists($offset): bool {
+ return $this->hasKey($offset);
+ }
+
+ public function offsetSet($offset, $value): void {
+ $this->set($offset, $value);
+ }
+
+ /**
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($offset) {
+ return $this->get($offset);
+ }
+
+ public function offsetUnset($offset): void {
+ $this->remove($offset);
+ }
+
+ public function collect(Request $request, Response $response, ?\Throwable $exception = null): void {
+ // Nothing to do here $data is already ready
+ }
+
+ public function getName(): string {
+ return 'cache/' . $this->type . '/' . $this->prefix;
+ }
+
+ public static function isAvailable(): bool {
+ return true;
+ }
+}
diff --git a/lib/private/Memcache/Redis.php b/lib/private/Memcache/Redis.php
new file mode 100644
index 00000000000..f8c51570c4f
--- /dev/null
+++ b/lib/private/Memcache/Redis.php
@@ -0,0 +1,222 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Memcache;
+
+use OCP\IMemcacheTTL;
+
+class Redis extends Cache implements IMemcacheTTL {
+ /** name => [script, sha1] */
+ public const LUA_SCRIPTS = [
+ 'dec' => [
+ 'if redis.call("exists", KEYS[1]) == 1 then return redis.call("decrby", KEYS[1], ARGV[1]) else return "NEX" end',
+ '720b40cb66cef1579f2ef16ec69b3da8c85510e9',
+ ],
+ 'cas' => [
+ 'if redis.call("get", KEYS[1]) == ARGV[1] then redis.call("set", KEYS[1], ARGV[2]) return 1 else return 0 end',
+ '94eac401502554c02b811e3199baddde62d976d4',
+ ],
+ 'cad' => [
+ 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end',
+ 'cf0e94b2e9ffc7e04395cf88f7583fc309985910',
+ ],
+ 'ncad' => [
+ 'if redis.call("get", KEYS[1]) ~= ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end',
+ '75526f8048b13ce94a41b58eee59c664b4990ab2',
+ ],
+ 'caSetTtl' => [
+ 'if redis.call("get", KEYS[1]) == ARGV[1] then redis.call("expire", KEYS[1], ARGV[2]) return 1 else return 0 end',
+ 'fa4acbc946d23ef41d7d3910880b60e6e4972d72',
+ ],
+ ];
+
+ private const MAX_TTL = 30 * 24 * 60 * 60; // 1 month
+
+ /**
+ * @var \Redis|\RedisCluster $cache
+ */
+ private static $cache = null;
+
+ public function __construct($prefix = '', string $logFile = '') {
+ parent::__construct($prefix);
+ }
+
+ /**
+ * @return \Redis|\RedisCluster|null
+ * @throws \Exception
+ */
+ public function getCache() {
+ if (is_null(self::$cache)) {
+ self::$cache = \OC::$server->get('RedisFactory')->getInstance();
+ }
+ return self::$cache;
+ }
+
+ public function get($key) {
+ $result = $this->getCache()->get($this->getPrefix() . $key);
+ if ($result === false) {
+ return null;
+ }
+
+ return self::decodeValue($result);
+ }
+
+ public function set($key, $value, $ttl = 0) {
+ $value = self::encodeValue($value);
+ if ($ttl === 0) {
+ // having infinite TTL can lead to leaked keys as the prefix changes with version upgrades
+ $ttl = self::DEFAULT_TTL;
+ }
+ $ttl = min($ttl, self::MAX_TTL);
+ return $this->getCache()->setex($this->getPrefix() . $key, $ttl, $value);
+ }
+
+ public function hasKey($key) {
+ return (bool)$this->getCache()->exists($this->getPrefix() . $key);
+ }
+
+ public function remove($key) {
+ if ($this->getCache()->unlink($this->getPrefix() . $key)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public function clear($prefix = '') {
+ // TODO: this is slow and would fail with Redis cluster
+ $prefix = $this->getPrefix() . $prefix . '*';
+ $keys = $this->getCache()->keys($prefix);
+ $deleted = $this->getCache()->del($keys);
+
+ return (is_array($keys) && (count($keys) === $deleted));
+ }
+
+ /**
+ * Set a value in the cache if it's not already stored
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param int $ttl Time To Live in seconds. Defaults to 60*60*24
+ * @return bool
+ */
+ public function add($key, $value, $ttl = 0) {
+ $value = self::encodeValue($value);
+ if ($ttl === 0) {
+ // having infinite TTL can lead to leaked keys as the prefix changes with version upgrades
+ $ttl = self::DEFAULT_TTL;
+ }
+ $ttl = min($ttl, self::MAX_TTL);
+
+ $args = ['nx'];
+ $args['ex'] = $ttl;
+
+ return $this->getCache()->set($this->getPrefix() . $key, $value, $args);
+ }
+
+ /**
+ * Increase a stored number
+ *
+ * @param string $key
+ * @param int $step
+ * @return int | bool
+ */
+ public function inc($key, $step = 1) {
+ return $this->getCache()->incrBy($this->getPrefix() . $key, $step);
+ }
+
+ /**
+ * Decrease a stored number
+ *
+ * @param string $key
+ * @param int $step
+ * @return int | bool
+ */
+ public function dec($key, $step = 1) {
+ $res = $this->evalLua('dec', [$key], [$step]);
+ return ($res === 'NEX') ? false : $res;
+ }
+
+ /**
+ * Compare and set
+ *
+ * @param string $key
+ * @param mixed $old
+ * @param mixed $new
+ * @return bool
+ */
+ public function cas($key, $old, $new) {
+ $old = self::encodeValue($old);
+ $new = self::encodeValue($new);
+
+ return $this->evalLua('cas', [$key], [$old, $new]) > 0;
+ }
+
+ /**
+ * Compare and delete
+ *
+ * @param string $key
+ * @param mixed $old
+ * @return bool
+ */
+ public function cad($key, $old) {
+ $old = self::encodeValue($old);
+
+ return $this->evalLua('cad', [$key], [$old]) > 0;
+ }
+
+ public function ncad(string $key, mixed $old): bool {
+ $old = self::encodeValue($old);
+
+ return $this->evalLua('ncad', [$key], [$old]) > 0;
+ }
+
+ public function setTTL($key, $ttl) {
+ if ($ttl === 0) {
+ // having infinite TTL can lead to leaked keys as the prefix changes with version upgrades
+ $ttl = self::DEFAULT_TTL;
+ }
+ $ttl = min($ttl, self::MAX_TTL);
+ $this->getCache()->expire($this->getPrefix() . $key, $ttl);
+ }
+
+ public function getTTL(string $key): int|false {
+ $ttl = $this->getCache()->ttl($this->getPrefix() . $key);
+ return $ttl > 0 ? (int)$ttl : false;
+ }
+
+ public function compareSetTTL(string $key, mixed $value, int $ttl): bool {
+ $value = self::encodeValue($value);
+
+ return $this->evalLua('caSetTtl', [$key], [$value, $ttl]) > 0;
+ }
+
+ public static function isAvailable(): bool {
+ return \OC::$server->get('RedisFactory')->isAvailable();
+ }
+
+ protected function evalLua(string $scriptName, array $keys, array $args) {
+ $keys = array_map(fn ($key) => $this->getPrefix() . $key, $keys);
+ $args = array_merge($keys, $args);
+ $script = self::LUA_SCRIPTS[$scriptName];
+
+ $result = $this->getCache()->evalSha($script[1], $args, count($keys));
+ if ($result === false) {
+ $result = $this->getCache()->eval($script[0], $args, count($keys));
+ }
+
+ return $result;
+ }
+
+ protected static function encodeValue(mixed $value): string {
+ return is_int($value) ? (string)$value : json_encode($value);
+ }
+
+ protected static function decodeValue(string $value): mixed {
+ return is_numeric($value) ? (int)$value : json_decode($value, true);
+ }
+}
diff --git a/lib/private/Memcache/WithLocalCache.php b/lib/private/Memcache/WithLocalCache.php
new file mode 100644
index 00000000000..0fc5d310801
--- /dev/null
+++ b/lib/private/Memcache/WithLocalCache.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Memcache;
+
+use OCP\Cache\CappedMemoryCache;
+use OCP\ICache;
+
+/**
+ * Wrap a cache instance with an extra later of local, in-memory caching
+ */
+class WithLocalCache implements ICache {
+ private ICache $inner;
+ private CappedMemoryCache $cached;
+
+ public function __construct(ICache $inner, int $localCapacity = 512) {
+ $this->inner = $inner;
+ $this->cached = new CappedMemoryCache($localCapacity);
+ }
+
+ public function get($key) {
+ if (isset($this->cached[$key])) {
+ return $this->cached[$key];
+ } else {
+ $value = $this->inner->get($key);
+ if (!is_null($value)) {
+ $this->cached[$key] = $value;
+ }
+ return $value;
+ }
+ }
+
+ public function set($key, $value, $ttl = 0) {
+ $this->cached[$key] = $value;
+ return $this->inner->set($key, $value, $ttl);
+ }
+
+ public function hasKey($key) {
+ return isset($this->cached[$key]) || $this->inner->hasKey($key);
+ }
+
+ public function remove($key) {
+ unset($this->cached[$key]);
+ return $this->inner->remove($key);
+ }
+
+ public function clear($prefix = '') {
+ $this->cached->clear();
+ return $this->inner->clear($prefix);
+ }
+
+ public static function isAvailable(): bool {
+ return false;
+ }
+}