Merge pull request #54383 from nextcloud/feat/cache-app-config

feat(AppConfig): cache the config if local cache is available
pull/54377/head
Ferdinand Thiessen 2 months ago committed by GitHub
commit 6d5dd4b389
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 19
      build/psalm-baseline.xml
  2. 9
      config/config.sample.php
  3. 4
      core/Command/Encryption/Enable.php
  4. 13
      cypress/support/commands.ts
  5. 2
      lib/private/AllConfig.php
  6. 176
      lib/private/AppConfig.php
  7. 2
      lib/private/Installer.php
  8. 169
      lib/private/Memcache/Factory.php
  9. 73
      lib/private/Server.php
  10. 2
      lib/private/legacy/OC_App.php
  11. 4
      lib/public/IConfig.php
  12. 8
      tests/Core/Command/Encryption/EnableTest.php
  13. 21
      tests/lib/App/AppManagerTest.php
  14. 1515
      tests/lib/AppConfigIntegrationTest.php
  15. 1639
      tests/lib/AppConfigTest.php
  16. 10
      tests/lib/Memcache/FactoryTest.php

@ -2980,9 +2980,6 @@
<code><![CDATA[getAppValue]]></code>
<code><![CDATA[setAppValue]]></code>
</DeprecatedMethod>
<NullArgument>
<code><![CDATA[null]]></code>
</NullArgument>
</file>
<file src="core/Command/Encryption/MigrateKeyStorage.php">
<DeprecatedClass>
@ -3301,11 +3298,6 @@
<code><![CDATA[ActivitySettings[]]]></code>
</MoreSpecificReturnType>
</file>
<file src="lib/private/AllConfig.php">
<MoreSpecificImplementedParamType>
<code><![CDATA[$key]]></code>
</MoreSpecificImplementedParamType>
</file>
<file src="lib/private/App/DependencyAnalyzer.php">
<InvalidNullableReturnType>
<code><![CDATA[bool]]></code>
@ -3314,11 +3306,6 @@
<code><![CDATA[version_compare($first, $second, $operator)]]></code>
</NullableReturnStatement>
</file>
<file src="lib/private/AppConfig.php">
<NullableReturnStatement>
<code><![CDATA[$this->fastCache[$app][$key] ?? $default]]></code>
</NullableReturnStatement>
</file>
<file src="lib/private/AppFramework/Bootstrap/FunctionInjector.php">
<UndefinedMethod>
<code><![CDATA[getName]]></code>
@ -4015,9 +4002,6 @@
<code><![CDATA[false]]></code>
<code><![CDATA[false]]></code>
</InvalidArgument>
<NullArgument>
<code><![CDATA[null]]></code>
</NullArgument>
</file>
<file src="lib/private/IntegrityCheck/Checker.php">
<InvalidArrayAccess>
@ -4394,9 +4378,6 @@
<InvalidArgument>
<code><![CDATA[$groupsList]]></code>
</InvalidArgument>
<NullArgument>
<code><![CDATA[null]]></code>
</NullArgument>
</file>
<file src="lib/private/legacy/OC_Helper.php">
<InvalidArrayOffset>

@ -1792,6 +1792,15 @@ $CONFIG = [
*/
'cache_chunk_gc_ttl' => 60*60*24,
/**
* Enable caching of the app config values.
* If enabled the app config will be cached locally for a short TTL,
* reducing database load significatly on larger setups.
*
* Defaults to ``true``
*/
'cache_app_config' => true,
/**
* Using Object Store with Nextcloud
*/

@ -42,8 +42,8 @@ class Enable extends Command {
$output->writeln('<error>No encryption module is loaded</error>');
return 1;
}
$defaultModule = $this->config->getAppValue('core', 'default_encryption_module', null);
if ($defaultModule === null) {
$defaultModule = $this->config->getAppValue('core', 'default_encryption_module');
if ($defaultModule === '') {
$output->writeln('<error>No default module is set</error>');
return 1;
}

@ -246,3 +246,16 @@ Cypress.Commands.add('userFileExists', (user: string, path: string) => {
return cy.runCommand(`stat --printf="%s" "data/${user}/files/${path}"`, { failOnNonZeroExit: true })
.then((exec) => Number.parseInt(exec.stdout || '0'))
})
Cypress.Commands.add('runOccCommand', (command: string, options?: Partial<Cypress.ExecOptions>) => {
return cy.runCommand(`php ./occ ${command}`, options)
.then((context) =>
// OCC cannot clear the APCu cache
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(
command.startsWith('app:') || command.startsWith('config:')
? 3000 // clear APCu cache
: 0,
).then(() => context),
)
})

@ -195,7 +195,7 @@ class AllConfig implements IConfig {
* @deprecated 29.0.0 Use {@see IAppConfig} directly
*/
public function getAppValue($appName, $key, $default = '') {
return \OC::$server->get(AppConfig::class)->getValue($appName, $key, $default);
return \OC::$server->get(AppConfig::class)->getValue($appName, $key, $default) ?? $default;
}
/**

@ -14,8 +14,8 @@ use JsonException;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\Config\ConfigManager;
use OC\Config\PresetManager;
use OC\Memcache\Factory as CacheFactory;
use OCP\Config\Lexicon\Entry;
use OCP\Config\Lexicon\ILexicon;
use OCP\Config\Lexicon\Strictness;
use OCP\Config\ValueType;
use OCP\DB\Exception as DBException;
@ -24,6 +24,8 @@ use OCP\Exceptions\AppConfigIncorrectTypeException;
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;
@ -53,10 +55,12 @@ class AppConfig implements IAppConfig {
private const KEY_MAX_LENGTH = 64;
private const ENCRYPTION_PREFIX = '$AppConfigEncryption$';
private const ENCRYPTION_PREFIX_LENGTH = 21; // strlen(self::ENCRYPTION_PREFIX)
private const LOCAL_CACHE_KEY = 'OC\\AppConfig';
private const LOCAL_CACHE_TTL = 3;
/** @var array<string, array<string, mixed>> ['app_id' => ['config_key' => 'config_value']] */
/** @var array<string, array<string, string>> ['app_id' => ['config_key' => 'config_value']] */
private array $fastCache = []; // cache for normal config keys
/** @var array<string, array<string, mixed>> ['app_id' => ['config_key' => 'config_value']] */
/** @var array<string, array<string, string>> ['app_id' => ['config_key' => 'config_value']] */
private array $lazyCache = []; // cache for lazy config keys
/** @var array<string, array<string, int>> ['app_id' => ['config_key' => bitflag]] */
private array $valueTypes = []; // type for all config values
@ -67,6 +71,7 @@ class AppConfig implements IAppConfig {
private bool $ignoreLexiconAliases = false;
/** @var ?array<string, string> */
private ?array $appVersionsCache = null;
private ?ICache $localCache = null;
public function __construct(
protected IDBConnection $connection,
@ -75,7 +80,13 @@ class AppConfig implements IAppConfig {
private readonly PresetManager $presetManager,
protected LoggerInterface $logger,
protected ICrypto $crypto,
readonly CacheFactory $cacheFactory,
) {
if ($config->getSystemValueBool('cache_app_config', true) && $cacheFactory->isLocalCacheAvailable()) {
$cacheFactory->withServerVersionPrefix(function (ICacheFactory $factory) {
$this->localCache = $factory->createLocal();
});
}
}
/**
@ -85,7 +96,7 @@ class AppConfig implements IAppConfig {
* @since 7.0.0
*/
public function getApps(): array {
$this->loadConfigAll();
$this->loadConfig(lazy: true);
$apps = array_merge(array_keys($this->fastCache), array_keys($this->lazyCache));
sort($apps);
@ -103,7 +114,7 @@ class AppConfig implements IAppConfig {
*/
public function getKeys(string $app): array {
$this->assertParams($app);
$this->loadConfigAll($app);
$this->loadConfig($app, true);
$keys = array_merge(array_keys($this->fastCache[$app] ?? []), array_keys($this->lazyCache[$app] ?? []));
sort($keys);
@ -149,19 +160,16 @@ class AppConfig implements IAppConfig {
*/
public function hasKey(string $app, string $key, ?bool $lazy = false): bool {
$this->assertParams($app, $key);
$this->loadConfig($app, $lazy);
$this->loadConfig($app, $lazy ?? true);
$this->matchAndApplyLexiconDefinition($app, $key);
$hasLazy = isset($this->lazyCache[$app][$key]);
$hasFast = isset($this->fastCache[$app][$key]);
if ($lazy === null) {
$appCache = $this->getAllValues($app);
return isset($appCache[$key]);
}
if ($lazy) {
return isset($this->lazyCache[$app][$key]);
return $hasLazy || $hasFast;
} else {
return $lazy ? $hasLazy : $hasFast;
}
return isset($this->fastCache[$app][$key]);
}
/**
@ -175,7 +183,7 @@ class AppConfig implements IAppConfig {
*/
public function isSensitive(string $app, string $key, ?bool $lazy = false): bool {
$this->assertParams($app, $key);
$this->loadConfig(null, $lazy);
$this->loadConfig(null, $lazy ?? true);
$this->matchAndApplyLexiconDefinition($app, $key);
if (!isset($this->valueTypes[$app][$key])) {
@ -227,7 +235,7 @@ class AppConfig implements IAppConfig {
public function getAllValues(string $app, string $prefix = '', bool $filtered = false): array {
$this->assertParams($app, $prefix);
// if we want to filter values, we need to get sensitivity
$this->loadConfigAll($app);
$this->loadConfig($app, true);
// array_merge() will remove numeric keys (here config keys), so addition arrays instead
$values = $this->formatAppValues($app, ($this->fastCache[$app] ?? []) + ($this->lazyCache[$app] ?? []));
$values = array_filter(
@ -479,7 +487,7 @@ class AppConfig implements IAppConfig {
return $default;
}
$this->loadConfig($app, $lazy);
$this->loadConfig($app, $lazy ?? true);
/**
* We ignore check if mixed type is requested.
@ -551,7 +559,7 @@ class AppConfig implements IAppConfig {
}
$this->assertParams($app, $key);
$this->loadConfig($app, $lazy);
$this->loadConfig($app, $lazy ?? true);
if (!isset($this->valueTypes[$app][$key])) {
throw new AppConfigUnknownKeyException('unknown config key');
@ -788,7 +796,7 @@ class AppConfig implements IAppConfig {
if (!$this->matchAndApplyLexiconDefinition($app, $key, $lazy, $type)) {
return false; // returns false as database is not updated
}
$this->loadConfig(null, $lazy);
$this->loadConfig(null, $lazy ?? true);
$sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type);
$inserted = $refreshCache = false;
@ -803,7 +811,7 @@ class AppConfig implements IAppConfig {
* no update if key is already known with set lazy status and value is
* not different, unless sensitivity is switched from false to true.
*/
if ($origValue === $this->getTypedValue($app, $key, $value, $lazy, $type)
if ($origValue === $this->getTypedValue($app, $key, $value, $lazy ?? true, $type)
&& (!$sensitive || $this->isSensitive($app, $key, $lazy))) {
return false;
}
@ -835,7 +843,7 @@ class AppConfig implements IAppConfig {
if (!$inserted) {
$currType = $this->valueTypes[$app][$key] ?? 0;
if ($currType === 0) { // this might happen when switching lazy loading status
$this->loadConfigAll();
$this->loadConfig(lazy: true);
$currType = $this->valueTypes[$app][$key] ?? 0;
}
@ -856,7 +864,7 @@ class AppConfig implements IAppConfig {
&& ($type | self::VALUE_SENSITIVE) !== ($currType | self::VALUE_SENSITIVE)) {
try {
$currType = $this->convertTypeToString($currType);
$type = $this->convertTypeToString($type);
$this->convertTypeToString($type);
} catch (AppConfigIncorrectTypeException) {
// can be ignored, this was just needed for a better exception message.
}
@ -895,6 +903,7 @@ class AppConfig implements IAppConfig {
$this->fastCache[$app][$key] = $value;
}
$this->valueTypes[$app][$key] = $type;
$this->clearLocalCache();
return true;
}
@ -916,7 +925,7 @@ class AppConfig implements IAppConfig {
*/
public function updateType(string $app, string $key, int $type = self::VALUE_MIXED): bool {
$this->assertParams($app, $key);
$this->loadConfigAll();
$this->loadConfig(lazy: true);
$this->matchAndApplyLexiconDefinition($app, $key);
$this->isLazy($app, $key); // confirm key exists
@ -959,7 +968,7 @@ class AppConfig implements IAppConfig {
*/
public function updateSensitive(string $app, string $key, bool $sensitive): bool {
$this->assertParams($app, $key);
$this->loadConfigAll();
$this->loadConfig(lazy: true);
$this->matchAndApplyLexiconDefinition($app, $key);
try {
@ -1019,7 +1028,7 @@ class AppConfig implements IAppConfig {
*/
public function updateLazy(string $app, string $key, bool $lazy): bool {
$this->assertParams($app, $key);
$this->loadConfigAll();
$this->loadConfig(lazy: true);
$this->matchAndApplyLexiconDefinition($app, $key);
try {
@ -1055,7 +1064,7 @@ class AppConfig implements IAppConfig {
*/
public function getDetails(string $app, string $key): array {
$this->assertParams($app, $key);
$this->loadConfigAll();
$this->loadConfig(lazy: true);
$this->matchAndApplyLexiconDefinition($app, $key);
$lazy = $this->isLazy($app, $key);
@ -1198,6 +1207,7 @@ class AppConfig implements IAppConfig {
unset($this->lazyCache[$app][$key]);
unset($this->fastCache[$app][$key]);
unset($this->valueTypes[$app][$key]);
$this->clearLocalCache();
}
/**
@ -1227,12 +1237,13 @@ class AppConfig implements IAppConfig {
public function clearCache(bool $reload = false): void {
$this->lazyLoaded = $this->fastLoaded = false;
$this->lazyCache = $this->fastCache = $this->valueTypes = $this->configLexiconDetails = [];
$this->localCache?->remove(self::LOCAL_CACHE_KEY);
if (!$reload) {
return;
}
$this->loadConfigAll();
$this->loadConfig(lazy: true);
}
@ -1293,35 +1304,49 @@ class AppConfig implements IAppConfig {
}
}
private function loadConfigAll(?string $app = null): void {
$this->loadConfig($app, null);
}
/**
* Load normal config or config set as lazy loaded
*
* @param bool|null $lazy set to TRUE to load config set as lazy loaded, set to NULL to load all config
* @param bool $lazy set to TRUE to also load config values set as lazy loaded
*/
private function loadConfig(?string $app = null, ?bool $lazy = false): void {
private function loadConfig(?string $app = null, bool $lazy = false): void {
if ($this->isLoaded($lazy)) {
return;
}
// if lazy is null or true, we debug log
if (($lazy ?? true) !== false && $app !== null) {
if ($lazy === true && $app !== null) {
$exception = new \RuntimeException('The loading of lazy AppConfig values have been triggered by app "' . $app . '"');
$this->logger->debug($exception->getMessage(), ['exception' => $exception, 'app' => $app]);
}
$qb = $this->connection->getQueryBuilder();
$qb->from('appconfig');
$loadLazyOnly = $lazy && $this->isLoaded();
// we only need value from lazy when loadConfig does not specify it
$qb->select('appid', 'configkey', 'configvalue', 'type');
/** @var array<mixed> */
$cacheContent = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? [];
$includesLazyValues = !empty($cacheContent) && !empty($cacheContent['lazyCache']);
if (!empty($cacheContent) && (!$lazy || $includesLazyValues)) {
$this->valueTypes = $cacheContent['valueTypes'];
$this->fastCache = $cacheContent['fastCache'];
$this->fastLoaded = !empty($this->fastCache);
if ($includesLazyValues) {
$this->lazyCache = $cacheContent['lazyCache'];
$this->lazyLoaded = !empty($this->lazyCache);
}
return;
}
if ($lazy !== null) {
$qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT)));
// Otherwise no cache available and we need to fetch from database
$qb = $this->connection->getQueryBuilder();
$qb->from('appconfig')
->select('appid', 'configkey', 'configvalue', 'type');
if ($lazy === false) {
$qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
} else {
if ($loadLazyOnly) {
$qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT)));
}
$qb->addSelect('lazy');
}
@ -1329,56 +1354,34 @@ class AppConfig implements IAppConfig {
$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) {
if ($lazy && ((int)$row['lazy']) === 1) {
$this->lazyCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
} else {
$this->fastCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
}
$this->valueTypes[$row['appid']][$row['configkey']] = (int)($row['type'] ?? 0);
}
$result->closeCursor();
$this->setAsLoaded($lazy);
}
/**
* if $lazy is:
* - false: will returns true if fast config is loaded
* - true : will returns true if lazy config is loaded
* - null : will returns true if both config are loaded
*
* @param bool $lazy
*
* @return bool
*/
private function isLoaded(?bool $lazy): bool {
if ($lazy === null) {
return $this->lazyLoaded && $this->fastLoaded;
}
$result->closeCursor();
$this->localCache?->set(
self::LOCAL_CACHE_KEY,
[
'fastCache' => $this->fastCache,
'lazyCache' => $this->lazyCache,
'valueTypes' => $this->valueTypes,
],
self::LOCAL_CACHE_TTL,
);
return $lazy ? $this->lazyLoaded : $this->fastLoaded;
$this->fastLoaded = true;
$this->lazyLoaded = $lazy;
}
/**
* if $lazy is:
* - false: set fast config as loaded
* - true : set lazy config as loaded
* - null : set both config as loaded
*
* @param bool $lazy
* @param bool $lazy - If set to true then also check if lazy values are loaded
*/
private function setAsLoaded(?bool $lazy): void {
if ($lazy === null) {
$this->fastLoaded = true;
$this->lazyLoaded = true;
return;
}
if ($lazy) {
$this->lazyLoaded = true;
} else {
$this->fastLoaded = true;
}
private function isLoaded(bool $lazy = false): bool {
return $this->fastLoaded && (!$lazy || $this->lazyLoaded);
}
/**
@ -1386,7 +1389,7 @@ class AppConfig implements IAppConfig {
*
* @param string $app app
* @param string $key key
* @param string $default = null, default value if the key does not exist
* @param string $default - Default value if the key does not exist
*
* @return string the value or $default
* @deprecated 29.0.0 use getValue*()
@ -1394,7 +1397,7 @@ class AppConfig implements IAppConfig {
* This function gets a value from the appconfig table. If the key does
* not exist the default value will be returned
*/
public function getValue($app, $key, $default = null) {
public function getValue($app, $key, $default = '') {
$this->loadConfig($app);
$this->matchAndApplyLexiconDefinition($app, $key);
@ -1421,7 +1424,7 @@ class AppConfig implements IAppConfig {
* or enabled (lazy=lazy-2)
*
* this solution would remove the loading of config values from disabled app
* unless calling the method {@see loadConfigAll()}
* unless calling the method.
*/
return $this->setTypedValue($app, $key, (string)$value, false, self::VALUE_MIXED);
}
@ -1733,7 +1736,7 @@ class AppConfig implements IAppConfig {
*
* @return bool TRUE if conflict can be fully ignored, FALSE if action should be not performed
* @throws AppConfigUnknownKeyException if strictness implies exception
* @see ILexicon::getStrictness()
* @see \OCP\Config\Lexicon\ILexicon::getStrictness()
*/
private function applyLexiconStrictness(
?Strictness $strictness,
@ -1772,8 +1775,9 @@ class AppConfig implements IAppConfig {
$configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
foreach ($configLexicon?->getAppConfigs() ?? [] as $configEntry) {
$entries[$configEntry->getKey()] = $configEntry;
if ($configEntry->getRename() !== null) {
$aliases[$configEntry->getRename()] = $configEntry->getKey();
$newName = $configEntry->getRename();
if ($newName !== null) {
$aliases[$newName] = $configEntry->getKey();
}
}
@ -1819,4 +1823,8 @@ class AppConfig implements IAppConfig {
}
return $this->appVersionsCache;
}
private function clearLocalCache(): void {
$this->localCache?->remove(self::LOCAL_CACHE_KEY);
}
}

@ -546,7 +546,7 @@ class Installer {
while (false !== ($filename = readdir($dir))) {
if ($filename[0] !== '.' and is_dir($app_dir['path'] . "/$filename")) {
if (file_exists($app_dir['path'] . "/$filename/appinfo/info.xml")) {
if ($config->getAppValue($filename, 'installed_version', null) === null) {
if ($config->getAppValue($filename, 'installed_version') === '') {
$enabled = $appManager->isDefaultEnabled($filename);
if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
&& $config->getAppValue($filename, 'enabled') !== 'no') {

@ -7,72 +7,65 @@
*/
namespace OC\Memcache;
use Closure;
use OC\SystemConfig;
use OCP\Cache\CappedMemoryCache;
use OCP\IAppConfig;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IMemcache;
use OCP\Profiler\IProfiler;
use OCP\ServerVersion;
use Psr\Log\LoggerInterface;
class Factory implements ICacheFactory {
public const NULL_CACHE = NullCache::class;
private ?string $globalPrefix = null;
private LoggerInterface $logger;
protected ?string $globalPrefix = null;
/**
* @var ?class-string<ICache> $localCacheClass
* @var class-string<ICache> $localCacheClass
*/
private ?string $localCacheClass;
protected string $localCacheClass;
/**
* @var ?class-string<ICache> $distributedCacheClass
* @var class-string<ICache> $distributedCacheClass
*/
private ?string $distributedCacheClass;
protected string $distributedCacheClass;
/**
* @var ?class-string<IMemcache> $lockingCacheClass
* @var class-string<IMemcache> $lockingCacheClass
*/
private ?string $lockingCacheClass;
private string $logFile;
private IProfiler $profiler;
protected string $lockingCacheClass;
/**
* @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,
protected LoggerInterface $logger,
protected IProfiler $profiler,
protected ServerVersion $serverVersion,
?string $localCacheClass = null,
?string $distributedCacheClass = null,
?string $lockingCacheClass = null,
string $logFile = '',
protected 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 (!class_exists($localCacheClass)
|| !is_a($localCacheClass, ICache::class, true)
|| !$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.
@ -84,7 +77,11 @@ class Factory implements ICacheFactory {
]), $missingCacheHint);
}
}
if (!class_exists($distributedCacheClass) || !$distributedCacheClass::isAvailable()) {
if (!class_exists($distributedCacheClass)
|| !is_a($distributedCacheClass, ICache::class, true)
|| !$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.
@ -96,25 +93,51 @@ class Factory implements ICacheFactory {
]), $missingCacheHint);
}
}
if (!($lockingCacheClass && class_exists($lockingCacheClass) && $lockingCacheClass::isAvailable())) {
if (!$lockingCacheClass
|| !class_exists($lockingCacheClass)
|| !is_a($lockingCacheClass, IMemcache::class, true)
|| !$lockingCacheClass::isAvailable()
) {
// don't fall back since the fallback might not be suitable for storing lock
$lockingCacheClass = self::NULL_CACHE;
}
/** @var class-string<IMemcache> */
$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)();
protected function getGlobalPrefix(): string {
if ($this->globalPrefix === null) {
$config = \OCP\Server::get(SystemConfig::class);
$versions = [];
if ($config->getValue('installed', false)) {
$appConfig = \OCP\Server::get(IAppConfig::class);
$versions = $appConfig->getAppInstalledVersions();
}
$versions['core'] = implode('.', $this->serverVersion->getVersion());
$this->globalPrefix = hash('xxh128', implode(',', $versions));
}
return $this->globalPrefix;
}
/**
* Override the global prefix for a specific closure.
* This should only be used internally for bootstrapping purpose!
*
* @param string $globalPrefix - The prefix to use during the closure execution
* @param \Closure $closure - The closure with the cache factory as the first parameter
*/
public function withServerVersionPrefix(\Closure $closure): void {
$backupPrefix = $this->globalPrefix;
$this->globalPrefix = hash('xxh128', implode('.', $this->serverVersion->getVersion()));
$closure($this);
$this->globalPrefix = $backupPrefix;
}
/**
* create a cache instance for storing locks
*
@ -122,22 +145,17 @@ class Factory implements ICacheFactory {
* @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);
}
$cache = new $this->lockingCacheClass($this->getGlobalPrefix() . '/' . $prefix);
if ($this->lockingCacheClass === Redis::class) {
if ($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);
if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
$cache = new LoggerWrapperCache($cache, $this->logFile);
}
}
return $cache;
}
@ -149,22 +167,17 @@ class Factory implements ICacheFactory {
* @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);
}
$cache = new $this->distributedCacheClass($this->getGlobalPrefix() . '/' . $prefix);
if ($this->distributedCacheClass === Redis::class) {
if ($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);
if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
$cache = new LoggerWrapperCache($cache, $this->logFile);
}
}
return $cache;
}
@ -176,22 +189,17 @@ class Factory implements ICacheFactory {
* @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);
}
$cache = new $this->localCacheClass($this->getGlobalPrefix() . '/' . $prefix);
if ($this->localCacheClass === Redis::class) {
if ($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);
if ($this->logFile !== '' && is_writable(dirname($this->logFile)) && (!file_exists($this->logFile) || is_writable($this->logFile))) {
$cache = new LoggerWrapperCache($cache, $this->logFile);
}
}
return $cache;
}
@ -217,4 +225,11 @@ class Factory implements ICacheFactory {
public function isLocalCacheAvailable(): bool {
return $this->localCacheClass !== self::NULL_CACHE;
}
public function clearAll(): void {
$this->createLocal()->clear();
$this->createDistributed()->clear();
$this->createLocking()->clear();
$this->createInMemory()->clear();
}
}

@ -585,62 +585,37 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(IURLGenerator::class, URLGenerator::class);
$this->registerService(ICache::class, function ($c) {
return new Cache\File();
});
$this->registerAlias(ICache::class, Cache\File::class);
$this->registerService(Factory::class, function (Server $c) {
$profiler = $c->get(IProfiler::class);
$arrayCacheFactory = new \OC\Memcache\Factory(fn () => '', $c->get(LoggerInterface::class),
$profiler,
ArrayCache::class,
ArrayCache::class,
ArrayCache::class
);
$logger = $c->get(LoggerInterface::class);
$serverVersion = $c->get(ServerVersion::class);
/** @var SystemConfig $config */
$config = $c->get(SystemConfig::class);
/** @var ServerVersion $serverVersion */
$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;
}
} 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',
];
}
$v['core'] = implode(',', $serverVersion->getVersion());
$version = implode(',', array_keys($v)) . implode(',', $v);
$instanceId = \OC_Util::getInstanceId();
$path = \OC::$SERVERROOT;
return md5($instanceId . '-' . $version . '-' . $path);
};
return new \OC\Memcache\Factory($prefixClosure,
$c->get(LoggerInterface::class),
if (!$config->getValue('installed', false) || (defined('PHPUNIT_RUN') && PHPUNIT_RUN)) {
return new \OC\Memcache\Factory(
$logger,
$profiler,
/** @psalm-taint-escape callable */
$config->getValue('memcache.local', null),
/** @psalm-taint-escape callable */
$config->getValue('memcache.distributed', null),
/** @psalm-taint-escape callable */
$config->getValue('memcache.locking', null),
/** @psalm-taint-escape callable */
$config->getValue('redis_log_file')
$serverVersion,
ArrayCache::class,
ArrayCache::class,
ArrayCache::class
);
}
return $arrayCacheFactory;
return new \OC\Memcache\Factory(
$logger,
$profiler,
$serverVersion,
/** @psalm-taint-escape callable */
$config->getValue('memcache.local', null),
/** @psalm-taint-escape callable */
$config->getValue('memcache.distributed', null),
/** @psalm-taint-escape callable */
$config->getValue('memcache.locking', null),
/** @psalm-taint-escape callable */
$config->getValue('redis_log_file')
);
});
$this->registerAlias(ICacheFactory::class, Factory::class);

@ -685,7 +685,7 @@ class OC_App {
//set remote/public handlers
if (array_key_exists('ocsid', $appData)) {
\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
} elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) {
} elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid') !== '') {
\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
}
foreach ($appData['remote'] as $name => $path) {

@ -112,8 +112,8 @@ interface IConfig {
* Writes a new app wide value
*
* @param string $appName the appName that we want to store the value under
* @param string|float|int $key the key of the value, under which will be saved
* @param string $value the value that should be stored
* @param string $key the key of the value, under which will be saved
* @param string|float|int $value the value that should be stored
* @return void
* @since 6.0.0
* @deprecated 29.0.0 Use {@see IAppConfig} directly

@ -48,9 +48,9 @@ class EnableTest extends TestCase {
public static function dataEnable(): array {
return [
['no', null, [], true, 'Encryption enabled', 'No encryption module is loaded'],
['yes', null, [], false, 'Encryption is already enabled', 'No encryption module is loaded'],
['no', null, ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'No default module is set'],
['no', '', [], true, 'Encryption enabled', 'No encryption module is loaded'],
['yes', '', [], false, 'Encryption is already enabled', 'No encryption module is loaded'],
['no', '', ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'No default module is set'],
['no', 'OC_NO_MODULE', ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'The current default module does not exist: OC_NO_MODULE'],
['no', 'OC_TEST_MODULE', ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'Default module: OC_TEST_MODULE'],
];
@ -79,7 +79,7 @@ class EnableTest extends TestCase {
->method('getAppValue')
->willReturnMap([
['core', 'encryption_enabled', 'no', $oldStatus],
['core', 'default_encryption_module', null, $defaultModule],
['core', 'default_encryption_module', '', $defaultModule],
]);
}

@ -233,28 +233,25 @@ class AppManagerTest extends TestCase {
$this->manager->disableApp('files_trashbin');
}
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new AppEnableEvent('files_trashbin'));
$this->manager->enableApp('files_trashbin');
$this->assertEquals('yes', $this->appConfig->getValue('files_trashbin', 'enabled', 'no'));
}
public function testDisableApp(): void {
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new AppDisableEvent('files_trashbin'));
$this->manager->disableApp('files_trashbin');
$this->assertEquals('no', $this->appConfig->getValue('files_trashbin', 'enabled', 'no'));
}
public function testNotEnableIfNotInstalled(): void {
try {
$this->manager->enableApp('some_random_name_which_i_hope_is_not_an_app');
$this->assertFalse(true, 'If this line is reached the expected exception is not thrown.');
} catch (AppPathNotFoundException $e) {
// Exception is expected
$this->assertEquals('Could not find path for some_random_name_which_i_hope_is_not_an_app', $e->getMessage());
}
$this->expectException(AppPathNotFoundException::class);
$this->expectExceptionMessage('Could not find path for some_random_name_which_i_hope_is_not_an_app');
$this->appConfig->expects(self::never())
->method('setValue');
$this->assertEquals('no', $this->appConfig->getValue(
'some_random_name_which_i_hope_is_not_an_app', 'enabled', 'no'
));
$this->manager->enableApp('some_random_name_which_i_hope_is_not_an_app');
}
public function testEnableAppForGroups(): void {
@ -289,7 +286,9 @@ class AppManagerTest extends TestCase {
->with('test')
->willReturn('apps/test');
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new AppEnableEvent('test', ['group1', 'group2']));
$this->eventDispatcher->expects($this->once())
->method('dispatchTyped')
->with(new AppEnableEvent('test', ['group1', 'group2']));
$manager->enableAppForGroups('test', $groups);
$this->assertEquals('["group1","group2"]', $this->appConfig->getValue('test', 'enabled', 'no'));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -12,6 +12,7 @@ use OC\Memcache\Factory;
use OC\Memcache\NullCache;
use OCP\HintException;
use OCP\Profiler\IProfiler;
use OCP\ServerVersion;
use Psr\Log\LoggerInterface;
class Test_Factory_Available_Cache1 extends NullCache {
@ -111,7 +112,8 @@ class FactoryTest extends \Test\TestCase {
$expectedLocalCache, $expectedDistributedCache, $expectedLockingCache): void {
$logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
$profiler = $this->getMockBuilder(IProfiler::class)->getMock();
$factory = new Factory(fn () => 'abc', $logger, $profiler, $localCache, $distributedCache, $lockingCache);
$serverVersion = $this->createMock(ServerVersion::class);
$factory = new Factory($logger, $profiler, $serverVersion, $localCache, $distributedCache, $lockingCache);
$this->assertTrue(is_a($factory->createLocal(), $expectedLocalCache));
$this->assertTrue(is_a($factory->createDistributed(), $expectedDistributedCache));
$this->assertTrue(is_a($factory->createLocking(), $expectedLockingCache));
@ -123,13 +125,15 @@ class FactoryTest extends \Test\TestCase {
$logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
$profiler = $this->getMockBuilder(IProfiler::class)->getMock();
new Factory(fn () => 'abc', $logger, $profiler, $localCache, $distributedCache);
$serverVersion = $this->createMock(ServerVersion::class);
new Factory($logger, $profiler, $serverVersion, $localCache, $distributedCache);
}
public function testCreateInMemory(): void {
$logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
$profiler = $this->getMockBuilder(IProfiler::class)->getMock();
$factory = new Factory(fn () => 'abc', $logger, $profiler, null, null, null);
$serverVersion = $this->createMock(ServerVersion::class);
$factory = new Factory($logger, $profiler, $serverVersion, null, null, null);
$cache = $factory->createInMemory();
$cache->set('test', 48);

Loading…
Cancel
Save