feat(UserConfig): add caching for user config values

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2025-08-13 12:08:15 +02:00
parent 8a2b02ab9c
commit f0694dd12c
No known key found for this signature in database
GPG key ID: 45FAE7268762B400

View file

@ -23,6 +23,8 @@ use OCP\Config\ValueType;
use OCP\DB\Exception as DBException;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Security\ICrypto;
@ -52,6 +54,8 @@ class UserConfig implements IUserConfig {
private const INDEX_MAX_LENGTH = 64;
private const ENCRYPTION_PREFIX = '$UserConfigEncryption$';
private const ENCRYPTION_PREFIX_LENGTH = 22; // strlen(self::ENCRYPTION_PREFIX)
private const LOCAL_CACHE_PREFIX = self::class;
private const LOCAL_CACHE_TTL = 5;
/** @var array<string, array<string, array<string, mixed>>> [ass'user_id' => ['app_id' => ['key' => 'value']]] */
private array $fastCache = []; // cache for normal config keys
@ -67,6 +71,8 @@ class UserConfig implements IUserConfig {
private array $configLexiconDetails = [];
private bool $ignoreLexiconAliases = false;
private ?ICache $localCache = null;
public function __construct(
protected IDBConnection $connection,
protected IConfig $config,
@ -74,7 +80,11 @@ class UserConfig implements IUserConfig {
private readonly PresetManager $presetManager,
protected LoggerInterface $logger,
protected ICrypto $crypto,
ICacheFactory $cacheFactory,
) {
if ($config->getSystemValueBool('cache_user_config', true) && $cacheFactory->isLocalCacheAvailable()) {
$this->localCache = $cacheFactory->createLocal(self::LOCAL_CACHE_PREFIX);
}
}
/**
@ -114,7 +124,7 @@ class UserConfig implements IUserConfig {
*/
public function getApps(string $userId): array {
$this->assertParams($userId, allowEmptyApp: true);
$this->loadConfigAll($userId);
$this->loadConfig($userId, true);
$apps = array_merge(array_keys($this->fastCache[$userId] ?? []), array_keys($this->lazyCache[$userId] ?? []));
sort($apps);
@ -132,7 +142,7 @@ class UserConfig implements IUserConfig {
*/
public function getKeys(string $userId, string $app): array {
$this->assertParams($userId, $app);
$this->loadConfigAll($userId);
$this->loadConfig($userId, true);
// array_merge() will remove numeric keys (here config keys), so addition arrays instead
$keys = array_map('strval', array_keys(($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? [])));
sort($keys);
@ -265,7 +275,7 @@ class UserConfig implements IUserConfig {
): array {
$this->assertParams($userId, $app, $prefix);
// if we want to filter values, we need to get sensitivity
$this->loadConfigAll($userId);
$this->loadConfig($userId, true);
// array_merge() will remove numeric keys (here config keys), so addition arrays instead
$values = array_filter(
$this->formatAppValues($userId, $app, ($this->fastCache[$userId][$app] ?? []) + ($this->lazyCache[$userId][$app] ?? []), $filtered),
@ -289,7 +299,7 @@ class UserConfig implements IUserConfig {
*/
public function getAllValues(string $userId, bool $filtered = false): array {
$this->assertParams($userId, allowEmptyApp: true);
$this->loadConfigAll($userId);
$this->loadConfig($userId, true);
$result = [];
foreach ($this->getApps($userId) as $app) {
@ -1148,7 +1158,7 @@ class UserConfig implements IUserConfig {
if (!$inserted) {
$currType = $this->valueDetails[$userId][$app][$key]['type'] ?? null;
if ($currType === null) { // this might happen when switching lazy loading status
$this->loadConfigAll($userId);
$this->loadConfig($userId, true);
$currType = $this->valueDetails[$userId][$app][$key]['type'];
}
@ -1209,6 +1219,7 @@ class UserConfig implements IUserConfig {
'type' => $type,
'flags' => $flags
];
$this->updateCache($userId);
return true;
}
@ -1231,7 +1242,7 @@ class UserConfig implements IUserConfig {
*/
public function updateType(string $userId, string $app, string $key, ValueType $type = ValueType::MIXED): bool {
$this->assertParams($userId, $app, $key);
$this->loadConfigAll($userId);
$this->loadConfig($userId, true);
$this->matchAndApplyLexiconDefinition($userId, $app, $key);
$this->isLazy($userId, $app, $key); // confirm key exists
@ -1244,6 +1255,7 @@ class UserConfig implements IUserConfig {
$update->executeStatement();
$this->valueDetails[$userId][$app][$key]['type'] = $type;
$this->updateCache($userId);
return true;
}
@ -1261,7 +1273,7 @@ class UserConfig implements IUserConfig {
*/
public function updateSensitive(string $userId, string $app, string $key, bool $sensitive): bool {
$this->assertParams($userId, $app, $key);
$this->loadConfigAll($userId);
$this->loadConfig($userId, true);
$this->matchAndApplyLexiconDefinition($userId, $app, $key);
try {
@ -1303,6 +1315,7 @@ class UserConfig implements IUserConfig {
$update->executeStatement();
$this->valueDetails[$userId][$app][$key]['flags'] = $flags;
$this->updateCache($userId);
return true;
}
@ -1348,7 +1361,7 @@ class UserConfig implements IUserConfig {
*/
public function updateIndexed(string $userId, string $app, string $key, bool $indexed): bool {
$this->assertParams($userId, $app, $key);
$this->loadConfigAll($userId);
$this->loadConfig($userId, true);
$this->matchAndApplyLexiconDefinition($userId, $app, $key);
try {
@ -1389,6 +1402,7 @@ class UserConfig implements IUserConfig {
$update->executeStatement();
$this->valueDetails[$userId][$app][$key]['flags'] = $flags;
$this->updateCache($userId);
return true;
}
@ -1432,7 +1446,7 @@ class UserConfig implements IUserConfig {
*/
public function updateLazy(string $userId, string $app, string $key, bool $lazy): bool {
$this->assertParams($userId, $app, $key);
$this->loadConfigAll($userId);
$this->loadConfig($userId, true);
$this->matchAndApplyLexiconDefinition($userId, $app, $key);
try {
@ -1493,7 +1507,7 @@ class UserConfig implements IUserConfig {
*/
public function getDetails(string $userId, string $app, string $key): array {
$this->assertParams($userId, $app, $key);
$this->loadConfigAll($userId);
$this->loadConfig($userId, true);
$this->matchAndApplyLexiconDefinition($userId, $app, $key);
$lazy = $this->isLazy($userId, $app, $key);
@ -1555,6 +1569,7 @@ class UserConfig implements IUserConfig {
unset($this->lazyCache[$userId][$app][$key]);
unset($this->fastCache[$userId][$app][$key]);
unset($this->valueDetails[$userId][$app][$key]);
$this->updateCache($userId);
}
/**
@ -1618,12 +1633,13 @@ class UserConfig implements IUserConfig {
$this->assertParams($userId, allowEmptyApp: true);
$this->lazyLoaded[$userId] = $this->fastLoaded[$userId] = false;
$this->lazyCache[$userId] = $this->fastCache[$userId] = $this->valueDetails[$userId] = [];
$this->localCache?->remove($userId);
if (!$reload) {
return;
}
$this->loadConfigAll($userId);
$this->loadConfig($userId, true);
}
/**
@ -1632,6 +1648,7 @@ class UserConfig implements IUserConfig {
* @since 31.0.0
*/
public function clearCacheAll(): void {
$this->localCache?->clear();
$this->lazyLoaded = $this->fastLoaded = [];
$this->lazyCache = $this->fastCache = $this->valueDetails = $this->configLexiconDetails = [];
}
@ -1698,40 +1715,51 @@ class UserConfig implements IUserConfig {
}
}
private function loadConfigAll(string $userId): void {
$this->loadConfig($userId, 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 options marked as lazy loaded
*/
private function loadConfig(string $userId, ?bool $lazy = false): void {
private function loadConfig(string $userId, bool $lazy = false): void {
if ($this->isLoaded($userId, $lazy)) {
return;
}
if (($lazy ?? true) !== false) { // if lazy is null or true, we debug log
if ($lazy === true) {
$this->logger->debug('The loading of lazy UserConfig values have been requested', ['exception' => new \RuntimeException('ignorable exception')]);
}
/** @var array<mixed> */
$cacheContent = $this->localCache?->get($userId) ?? [];
$includesLazyValues = !empty($cacheContent) && !empty($cacheContent['lazyCache']);
if (!empty($cacheContent) && (!$lazy || $includesLazyValues)) {
$this->valueDetails[$userId] = $cacheContent['valueDetails'];
$this->fastCache[$userId] = $cacheContent['fastCache'];
$this->fastLoaded[$userId] = true;
if ($includesLazyValues) {
$this->lazyCache[$userId] = $cacheContent['lazyCache'];
$this->lazyLoaded[$userId] = true;
}
return;
}
$qb = $this->connection->getQueryBuilder();
$qb->from('preferences');
$qb->select('appid', 'configkey', 'configvalue', 'type', 'flags');
$qb->where($qb->expr()->eq('userid', $qb->createNamedParameter($userId)));
// we only need value from lazy when loadConfig does not specify it
if ($lazy !== null) {
$qb->andWhere($qb->expr()->eq('lazy', $qb->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT)));
} else {
if ($lazy) {
$qb->addSelect('lazy');
}
if ($lazy === false || $this->isLoaded($userId, false)) {
// we only filter by lazy if not requested or if we already have the non-lazy
$qb->andWhere($qb->expr()->eq('lazy', $qb->createNamedParameter($lazy ? 1 : 0, IQueryBuilder::PARAM_INT)));
}
$result = $qb->executeQuery();
$rows = $result->fetchAll();
foreach ($rows as $row) {
if (($row['lazy'] ?? ($lazy ?? 0) ? 1 : 0) === 1) {
if ((($row['lazy'] ?? $lazy) ? 1 : 0) === 1) {
$this->lazyCache[$userId][$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
} else {
$this->fastCache[$userId][$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
@ -1740,6 +1768,7 @@ class UserConfig implements IUserConfig {
}
$result->closeCursor();
$this->setAsLoaded($userId, $lazy);
$this->updateCache($userId);
}
/**
@ -2036,4 +2065,16 @@ class UserConfig implements IUserConfig {
public function ignoreLexiconAliases(bool $ignore): void {
$this->ignoreLexiconAliases = $ignore;
}
private function updateCache(string $userId): void {
$this->localCache?->set(
$userId,
[
'fastCache' => $this->fastCache[$userId],
'lazyCache' => $this->lazyCache[$userId],
'valueDetails' => $this->valueDetails[$userId],
],
self::LOCAL_CACHE_TTL,
);
}
}