diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 476adbb93e0bd..3c3d0cd7de026 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -22,6 +22,8 @@ use OCP\Exceptions\AppConfigTypeConflictException; use OCP\Exceptions\AppConfigUnknownKeyException; use OCP\IAppConfig; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\IDBConnection; use OCP\Security\ICrypto; @@ -68,11 +70,15 @@ class AppConfig implements IAppConfig { /** @var ?array */ private ?array $appVersionsCache = null; + private ?ICache $localCache; + public function __construct( protected IDBConnection $connection, protected LoggerInterface $logger, protected ICrypto $crypto, + ICacheFactory $cacheFactory, ) { + $this->localCache = $cacheFactory->isLocalCacheAvailable() ? $cacheFactory->createLocal() : null; } /** @@ -859,6 +865,8 @@ private function setTypedValue( } $this->valueTypes[$app][$key] = $type; + $this->clearLocalCache(); + return true; } @@ -1118,6 +1126,7 @@ public function deleteKey(string $app, string $key): void { unset($this->lazyCache[$app][$key]); unset($this->fastCache[$app][$key]); unset($this->valueTypes[$app][$key]); + $this->clearLocalCache(); } /** @@ -1147,6 +1156,7 @@ public function deleteApp(string $app): void { public function clearCache(bool $reload = false): void { $this->lazyLoaded = $this->fastLoaded = false; $this->lazyCache = $this->fastCache = $this->valueTypes = []; + $this->clearLocalCache(); if (!$reload) { return; @@ -1217,6 +1227,21 @@ private function loadConfigAll(?string $app = null): void { $this->loadConfig($app, null); } + private function getLocalCacheKey(?bool $lazy): string { + return 'app-config' . ($lazy !== null ? '-' . ($lazy === true ? 'lazy' : 'non-lazy') : ''); + } + + private function clearLocalCache(): void { + if ($this->localCache === null) { + return; + } + + $keys = array_map(fn (?bool $lazy) => $this->getLocalCacheKey($lazy), [null, true, false]); + foreach ($keys as $key) { + $this->localCache->remove($key); + } + } + /** * Load normal config or config set as lazy loaded * @@ -1233,20 +1258,30 @@ private function loadConfig(?string $app = null, ?bool $lazy = false): void { $this->logger->debug($exception->getMessage(), ['exception' => $exception, 'app' => $app]); } - $qb = $this->connection->getQueryBuilder(); - $qb->from('appconfig'); + $cacheKey = $this->getLocalCacheKey($lazy); - // we only need value from lazy when loadConfig does not specify it - $qb->select('appid', 'configkey', 'configvalue', 'type'); + $rows = $this->localCache?->get($cacheKey); + if ($rows === null) { + $qb = $this->connection->getQueryBuilder(); + $qb->from('appconfig'); - if ($lazy !== null) { - $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))); - } else { - $qb->addSelect('lazy'); + // we only need value from lazy when loadConfig does not specify it + $qb->select('appid', 'configkey', 'configvalue', 'type'); + + if ($lazy !== null) { + $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT))); + } else { + $qb->addSelect('lazy'); + } + + $result = $qb->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + // The TTL is low to avoid syncing issues in clustered setups, but it still greatly improves performance when many requests per second are sent. + $this->localCache?->set($cacheKey, $rows, 1); } - $result = $qb->executeQuery(); - $rows = $result->fetchAll(); foreach ($rows as $row) { // most of the time, 'lazy' is not in the select because its value is already known if (($row['lazy'] ?? ($lazy ?? 0) ? 1 : 0) === 1) { @@ -1256,7 +1291,6 @@ private function loadConfig(?string $app = null, ?bool $lazy = false): void { } $this->valueTypes[$row['appid']][$row['configkey']] = (int)($row['type'] ?? 0); } - $result->closeCursor(); $this->setAsLoaded($lazy); } diff --git a/lib/private/Memcache/Factory.php b/lib/private/Memcache/Factory.php index b54189937fc12..ff2812fe8648c 100644 --- a/lib/private/Memcache/Factory.php +++ b/lib/private/Memcache/Factory.php @@ -42,7 +42,7 @@ class Factory implements ICacheFactory { private IProfiler $profiler; /** - * @param Closure $globalPrefixClosure + * @param Closure(self) $globalPrefixClosure * @param LoggerInterface $logger * @param ?class-string $localCacheClass * @param ?class-string $distributedCacheClass @@ -110,7 +110,7 @@ public function __construct( private function getGlobalPrefix(): ?string { if (is_null($this->globalPrefix)) { - $this->globalPrefix = ($this->globalPrefixClosure)(); + $this->globalPrefix = ($this->globalPrefixClosure)($this); } return $this->globalPrefix; } @@ -175,10 +175,14 @@ public function createDistributed(string $prefix = ''): ICache { * @param string $prefix * @return ICache */ - public function createLocal(string $prefix = ''): ICache { - $globalPrefix = $this->getGlobalPrefix(); - if (is_null($globalPrefix)) { - return new ArrayCache($prefix); + public function createLocal(string $prefix = '', bool $disableGlobalPrefix = false): ICache { + if ($disableGlobalPrefix) { + $globalPrefix = ''; + } else { + $globalPrefix = $this->getGlobalPrefix(); + if (is_null($globalPrefix)) { + return new ArrayCache($prefix); + } } assert($this->localCacheClass !== null); diff --git a/lib/private/Server.php b/lib/private/Server.php index 4b88a446405a5..874103ec3d37e 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -14,6 +14,7 @@ use OC\App\AppManager; use OC\App\AppStore\Bundles\BundleFetcher; use OC\App\AppStore\Fetcher\AppFetcher; +use OC\App\InfoParser; use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Http\Request; use OC\AppFramework\Http\RequestId; @@ -163,7 +164,6 @@ use OCP\Files\Template\ITemplateManager; use OCP\FilesMetadata\IFilesMetadataManager; use OCP\FullTextSearch\IFullTextSearchManager; -use OCP\GlobalScale\IConfig; use OCP\Group\ISubAdmin; use OCP\Http\Client\IClientService; use OCP\IAppConfig; @@ -602,24 +602,36 @@ public function __construct($webRoot, \OC\Config $config) { $serverVersion = $c->get(ServerVersion::class); if ($config->getValue('installed', false) && !(defined('PHPUNIT_RUN') && PHPUNIT_RUN)) { - $logQuery = $config->getValue('log_query'); - $prefixClosure = function () use ($logQuery, $serverVersion): ?string { - if (!$logQuery) { - try { - $v = \OCP\Server::get(IAppConfig::class)->getAppInstalledVersions(true); - } catch (\Doctrine\DBAL\Exception $e) { - // Database service probably unavailable - // Probably related to https://github.com/nextcloud/server/issues/37424 - return null; + $prefixClosure = function (Factory $factory) use ($serverVersion): string { + // It is impossible to rely on OC_App or AppManager at this point, as they rely on the cache itself. + $appInfoFiles = []; + foreach (\OC::$APPSROOTS as $apps_dir) { + if (!is_readable($apps_dir['path'])) { + continue; + } + + $dh = opendir($apps_dir['path']); + if (!is_resource($dh)) { + continue; + } + + while (($file = readdir($dh)) !== false) { + if ( + $file[0] !== '.' + && is_dir($apps_dir['path'] . '/' . $file) + && is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml') + ) { + $appInfoFiles[$file] = $apps_dir['path'] . '/' . $file . '/appinfo/info.xml'; + } } - } else { - // If the log_query is enabled, we can not get the app versions - // as that does a query, which will be logged and the logging - // depends on redis and here we are back again in the same function. - $v = [ - 'log_query' => 'enabled', - ]; } + + // We must disable the global prefix here, in order to compute the global prefix in this closure and avoid infinite recursion. + $prefixlessCache = $factory->createLocal(disableGlobalPrefix: true); + + $infoParser = new InfoParser($prefixlessCache); + + $v = array_map(static fn ($file): string => (string)$infoParser->parse($file)['version'], $appInfoFiles); $v['core'] = implode(',', $serverVersion->getVersion()); $version = implode(',', array_keys($v)) . implode(',', $v); $instanceId = \OC_Util::getInstanceId(); diff --git a/tests/lib/AppConfigTest.php b/tests/lib/AppConfigTest.php index 408e4321d4f46..9f1a25b06c853 100644 --- a/tests/lib/AppConfigTest.php +++ b/tests/lib/AppConfigTest.php @@ -12,6 +12,7 @@ use OCP\Exceptions\AppConfigTypeConflictException; use OCP\Exceptions\AppConfigUnknownKeyException; use OCP\IAppConfig; +use OCP\ICacheFactory; use OCP\IDBConnection; use OCP\Security\ICrypto; use OCP\Server; @@ -29,6 +30,7 @@ class AppConfigTest extends TestCase { protected IDBConnection $connection; private LoggerInterface $logger; private ICrypto $crypto; + private ICacheFactory $cacheFactory; private array $originalConfig; @@ -91,6 +93,7 @@ protected function setUp(): void { $this->connection = Server::get(IDBConnection::class); $this->logger = Server::get(LoggerInterface::class); $this->crypto = Server::get(ICrypto::class); + $this->cacheFactory = Server::get(ICacheFactory::class); // storing current config and emptying the data table $sql = $this->connection->getQueryBuilder(); @@ -181,6 +184,7 @@ private function generateAppConfig(bool $preLoading = true): IAppConfig { $this->connection, $this->logger, $this->crypto, + $this->cacheFactory, ); $msg = ' generateAppConfig() failed to confirm cache status';