chore: merge valueDetails into fastCache and lazyCache to avoid out-of-sync issues

It feels like a UserConfigEntry class would make sense instead of the
 currently used array to represent an entry.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
pull/61413/head
Côme Chilliet 2 months ago
parent 4967f7bf48
commit 4e34261967
No known key found for this signature in database
GPG Key ID: A3E2F658B28C760A
  1. 171
      lib/private/Config/UserConfig.php
  2. 13
      tests/lib/Config/UserConfigTest.php

@ -57,12 +57,10 @@ class UserConfig implements IUserConfig {
private const ENCRYPTION_PREFIX = '$UserConfigEncryption$';
private const ENCRYPTION_PREFIX_LENGTH = 22; // strlen(self::ENCRYPTION_PREFIX)
/** @var CappedMemoryCache<array<string, array<string, mixed>>> [ass'user_id' => ['app_id' => ['key' => 'value']]] cache for normal config keys */
/** @var CappedMemoryCache<array<string, array<string, array{type: ValueType, flags: int, value: string}>>> cache for normal config keys */
private CappedMemoryCache $fastCache;
/** @var CappedMemoryCache<array<string, array<string, mixed>>> ['user_id' => ['app_id' => ['key' => 'value']]] cache for lazy config keys */
/** @var CappedMemoryCache<array<string, array<string, array{type: ValueType, flags: int, value: string}>>> cache for lazy config keys */
private CappedMemoryCache $lazyCache;
/** @var array<string, array<string, array<string, array<string, mixed>>>> ['user_id' => ['app_id' => ['key' => ['type' => ValueType, 'flags' => bitflag]]]] */
private array $valueDetails = []; // type for all config values
/** @var array<string, array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */
private array $configLexiconDetails = [];
private bool $ignoreLexiconAliases = false;
@ -204,11 +202,13 @@ class UserConfig implements IUserConfig {
$this->loadConfig($userId, $lazy);
$this->matchAndApplyLexiconDefinition($userId, $app, $key);
if (!isset($this->valueDetails[$userId][$app][$key])) {
throw new UnknownKeyException('unknown config key');
if (isset($this->fastCache[$userId][$app][$key])) {
return $this->isFlagged(self::FLAG_SENSITIVE, $this->fastCache[$userId][$app][$key]['flags']);
} elseif (isset($this->lazyCache[$userId][$app][$key])) {
return $this->isFlagged(self::FLAG_SENSITIVE, $this->lazyCache[$userId][$app][$key]['flags']);
} else {
throw new UnknownKeyException('Unknown config key ' . $app . '/' . $key);
}
return $this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags']);
}
/**
@ -229,11 +229,13 @@ class UserConfig implements IUserConfig {
$this->loadConfig($userId, $lazy);
$this->matchAndApplyLexiconDefinition($userId, $app, $key);
if (!isset($this->valueDetails[$userId][$app][$key])) {
throw new UnknownKeyException('unknown config key');
if (isset($this->fastCache[$userId][$app][$key])) {
return $this->isFlagged(self::FLAG_INDEXED, $this->fastCache[$userId][$app][$key]['flags']);
} elseif (isset($this->lazyCache[$userId][$app][$key])) {
return $this->isFlagged(self::FLAG_INDEXED, $this->lazyCache[$userId][$app][$key]['flags']);
} else {
throw new UnknownKeyException('Unknown config key ' . $app . '/' . $key);
}
return $this->isFlagged(self::FLAG_INDEXED, $this->valueDetails[$userId][$app][$key]['flags']);
}
/**
@ -350,11 +352,12 @@ class UserConfig implements IUserConfig {
$values = [];
foreach (array_keys($cache) as $app) {
if (isset($cache[$app][$key])) {
$value = $cache[$app][$key];
$valueDetail = $cache[$app][$key];
try {
$this->decryptSensitiveValue($userId, $app, $key, $value);
$value = $this->convertTypedValue($value, $typedAs ?? $this->getValueType($userId, $app, $key, $lazy));
$this->decryptSensitiveValue($userId, $app, $key, $valueDetail);
$value = $this->convertTypedValue($valueDetail['value'], $typedAs ?? $this->getValueType($userId, $app, $key, $lazy));
} catch (IncorrectTypeException|UnknownKeyException) {
$value = $valueDetail['value'];
}
$values[$app] = $value;
}
@ -792,12 +795,27 @@ class UserConfig implements IUserConfig {
$this->loadConfig($userId, $lazy);
/**
* - the pair $app/$key cannot exist in both array,
* - we should still return an existing non-lazy value even if current method
* is called with $lazy is true
*
* This way, lazyCache will be empty until the load for lazy config value is requested.
*/
if (isset($this->lazyCache[$userId][$app][$key])) {
$valueDetail = $this->lazyCache[$userId][$app][$key];
} elseif (isset($this->fastCache[$userId][$app][$key])) {
$valueDetail = $this->fastCache[$userId][$app][$key];
} else {
return $default;
}
/**
* We ignore check if mixed type is requested.
* If type of stored value is set as mixed, we don't filter.
* If type of stored value is defined, we compare with the one requested.
*/
$knownType = $this->valueDetails[$userId][$app][$key]['type'] ?? null;
$knownType = $valueDetail['type'] ?? null;
if ($type !== ValueType::MIXED
&& $knownType !== null
&& $knownType !== ValueType::MIXED
@ -806,22 +824,9 @@ class UserConfig implements IUserConfig {
throw new TypeConflictException('conflict with value type from database');
}
/**
* - the pair $app/$key cannot exist in both array,
* - we should still return an existing non-lazy value even if current method
* is called with $lazy is true
*
* This way, lazyCache will be empty until the load for lazy config value is requested.
*/
if (isset($this->lazyCache[$userId][$app][$key])) {
$value = $this->lazyCache[$userId][$app][$key];
} elseif (isset($this->fastCache[$userId][$app][$key])) {
$value = $this->fastCache[$userId][$app][$key];
} else {
return $default;
}
$this->decryptSensitiveValue($userId, $app, $key, $valueDetail);
$this->decryptSensitiveValue($userId, $app, $key, $value);
$value = $valueDetail['value'];
// in case the key was modified while running matchAndApplyLexiconDefinition() we are
// interested to check options in case a modification of the value is needed
@ -851,11 +856,13 @@ class UserConfig implements IUserConfig {
$this->loadConfig($userId, $lazy);
$this->matchAndApplyLexiconDefinition($userId, $app, $key);
if (!isset($this->valueDetails[$userId][$app][$key]['type'])) {
throw new UnknownKeyException('unknown config key');
if (isset($this->fastCache[$userId][$app][$key])) {
return $this->fastCache[$userId][$app][$key]['type'];
} elseif (isset($this->lazyCache[$userId][$app][$key])) {
return $this->lazyCache[$userId][$app][$key]['type'];
} else {
throw new UnknownKeyException('Unknown config key ' . $app . '/' . $key);
}
return $this->valueDetails[$userId][$app][$key]['type'];
}
/**
@ -877,11 +884,13 @@ class UserConfig implements IUserConfig {
$this->loadConfig($userId, $lazy);
$this->matchAndApplyLexiconDefinition($userId, $app, $key);
if (!isset($this->valueDetails[$userId][$app][$key])) {
throw new UnknownKeyException('unknown config key');
if (isset($this->fastCache[$userId][$app][$key])) {
return $this->fastCache[$userId][$app][$key]['flags'];
} elseif (isset($this->lazyCache[$userId][$app][$key])) {
return $this->lazyCache[$userId][$app][$key]['flags'];
} else {
throw new UnknownKeyException('Unknown config key ' . $app . '/' . $key);
}
return $this->valueDetails[$userId][$app][$key]['flags'];
}
/**
@ -1216,14 +1225,14 @@ class UserConfig implements IUserConfig {
* We cannot insert a new row, meaning we need to update an already existing one
*/
if (!$inserted) {
$currType = $this->valueDetails[$userId][$app][$key]['type'] ?? null;
$currType = $this->fastCache[$userId][$app][$key]['type'] ?? $this->lazyCache[$userId][$app][$key]['type'] ?? null;
if ($currType === null) { // this might happen when switching lazy loading status
$this->loadConfigAll($userId);
if (!isset($this->valueDetails[$userId][$app][$key])) {
if (!isset($this->fastCache[$userId][$app][$key]) && !isset($this->lazyCache[$userId][$app][$key])) {
throw new UnknownKeyException("unknown key $app $key for $userId even though $updateReason");
}
$currType = $this->valueDetails[$userId][$app][$key]['type'] ?? null;
$currType = $this->fastCache[$userId][$app][$key]['type'] ?? $this->lazyCache[$userId][$app][$key]['type'] ?? null;
}
/**
@ -1278,15 +1287,16 @@ class UserConfig implements IUserConfig {
}
// update local cache
if ($lazy) {
$this->lazyCache[$userId][$app][$key] = $value;
} else {
$this->fastCache[$userId][$app][$key] = $value;
}
$this->valueDetails[$userId][$app][$key] = [
$valueDetail = [
'value' => $value,
'type' => $type,
'flags' => $flags
];
if ($lazy) {
$this->lazyCache[$userId][$app][$key] = $valueDetail;
} else {
$this->fastCache[$userId][$app][$key] = $valueDetail;
}
return true;
}
@ -1311,7 +1321,7 @@ class UserConfig implements IUserConfig {
$this->assertParams($userId, $app, $key);
$this->loadConfigAll($userId);
$this->matchAndApplyLexiconDefinition($userId, $app, $key);
$this->isLazy($userId, $app, $key); // confirm key exists
$lazy = $this->isLazy($userId, $app, $key); // confirm key exists
$update = $this->connection->getQueryBuilder();
$update->update('preferences')
@ -1321,7 +1331,11 @@ class UserConfig implements IUserConfig {
->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
$update->executeStatement();
$this->valueDetails[$userId][$app][$key]['type'] = $type;
if ($lazy) {
$this->lazyCache[$userId][$app][$key]['type'] = $type;
} else {
$this->fastCache[$userId][$app][$key]['type'] = $type;
}
return true;
}
@ -1362,14 +1376,15 @@ class UserConfig implements IUserConfig {
throw new UnknownKeyException('unknown config key');
}
$value = $cache[$userId][$app][$key];
$valueDetail = $cache[$userId][$app][$key];
$flags = $this->getValueFlags($userId, $app, $key);
if ($sensitive) {
$flags |= self::FLAG_SENSITIVE;
$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($value);
$value = self::ENCRYPTION_PREFIX . $this->crypto->encrypt($valueDetail['value']);
} else {
$flags &= ~self::FLAG_SENSITIVE;
$this->decryptSensitiveValue($userId, $app, $key, $value);
$this->decryptSensitiveValue($userId, $app, $key, $valueDetail);
$value = $valueDetail['value'];
}
$update = $this->connection->getQueryBuilder();
@ -1381,7 +1396,7 @@ class UserConfig implements IUserConfig {
->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
$update->executeStatement();
$this->valueDetails[$userId][$app][$key]['flags'] = $flags;
$cache[$userId][$app][$key]['flags'] = $flags;
return true;
}
@ -1451,7 +1466,7 @@ class UserConfig implements IUserConfig {
throw new UnknownKeyException('unknown config key');
}
$value = $cache[$userId][$app][$key];
$value = $cache[$userId][$app][$key]['value'];
$flags = $this->getValueFlags($userId, $app, $key);
if ($indexed) {
$indexed = $value;
@ -1469,7 +1484,7 @@ class UserConfig implements IUserConfig {
->andWhere($update->expr()->eq('configkey', $update->createNamedParameter($key)));
$update->executeStatement();
$this->valueDetails[$userId][$app][$key]['flags'] = $flags;
$cache[$userId][$app][$key]['flags'] = $flags;
return true;
}
@ -1619,15 +1634,15 @@ class UserConfig implements IUserConfig {
throw new UnknownKeyException('unknown config key');
}
$value = $cache[$app][$key];
$valueDetail = $cache[$app][$key];
$sensitive = $this->isSensitive($userId, $app, $key, null);
$this->decryptSensitiveValue($userId, $app, $key, $value);
$this->decryptSensitiveValue($userId, $app, $key, $valueDetail);
return [
'userId' => $userId,
'app' => $app,
'key' => $key,
'value' => $value,
'value' => $valueDetail['value'],
'type' => $type->value,
'lazy' => $lazy,
'typeString' => $typeString,
@ -1658,7 +1673,6 @@ class UserConfig implements IUserConfig {
unset($this->lazyCache[$userId][$app][$key]);
unset($this->fastCache[$userId][$app][$key]);
unset($this->valueDetails[$userId][$app][$key]);
}
/**
@ -1726,7 +1740,6 @@ class UserConfig implements IUserConfig {
$this->assertParams($userId, allowEmptyApp: true);
unset($this->lazyCache[$userId]);
unset($this->fastCache[$userId]);
$this->valueDetails[$userId] = [];
if (!$reload) {
return;
@ -1744,7 +1757,7 @@ class UserConfig implements IUserConfig {
public function clearCacheAll(): void {
$this->lazyCache = new CappedMemoryCache();
$this->fastCache = new CappedMemoryCache();
$this->valueDetails = $this->configLexiconDetails = [];
$this->configLexiconDetails = [];
}
/**
@ -1759,7 +1772,6 @@ class UserConfig implements IUserConfig {
return [
'fastCache' => $this->fastCache->getData(),
'lazyCache' => $this->lazyCache->getData(),
'valueDetails' => $this->valueDetails,
];
}
@ -1857,12 +1869,16 @@ class UserConfig implements IUserConfig {
$rows = $result->fetchAllAssociative();
foreach ($rows as $row) {
$valueDetail = [
'value' => $row['configvalue'] ?? '',
'type' => ValueType::from((int)($row['type'] ?? 0)),
'flags' => (int)($row['flags'] ?? 0),
];
if ($this->migrationCompleted && (($row['lazy'] ?? ($lazy ?? 0) ? 1 : 0) === 1)) {
$this->lazyCache[$userId][$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
$this->lazyCache[$userId][$row['appid']][$row['configkey']] = $valueDetail;
} else {
$this->fastCache[$userId][$row['appid']][$row['configkey']] = $row['configvalue'] ?? '';
$this->fastCache[$userId][$row['appid']][$row['configkey']] = $valueDetail;
}
$this->valueDetails[$userId][$row['appid']][$row['configkey']] = ['type' => ValueType::from((int)($row['type'] ?? 0)), 'flags' => (int)($row['flags'] ?? 0)];
}
$result->closeCursor();
}
@ -1896,7 +1912,8 @@ class UserConfig implements IUserConfig {
* @return array<string, string|int|float|bool|array>
*/
private function formatAppValues(string $userId, string $app, array $values, bool $filtered = false): array {
foreach ($values as $key => $value) {
foreach ($values as $key => $valueDetail) {
$value = $valueDetail['value'];
//$key = (string)$key;
try {
$type = $this->getValueType($userId, $app, (string)$key);
@ -1904,12 +1921,13 @@ class UserConfig implements IUserConfig {
continue;
}
if ($this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags'] ?? 0)) {
if ($this->isFlagged(self::FLAG_SENSITIVE, $valueDetail['flags'] ?? 0)) {
if ($filtered) {
$value = IConfig::SENSITIVE_VALUE;
$type = ValueType::STRING;
} else {
$this->decryptSensitiveValue($userId, $app, (string)$key, $value);
$this->decryptSensitiveValue($userId, $app, (string)$key, $valueDetail);
$value = $valueDetail['value'];
}
}
@ -1949,28 +1967,25 @@ class UserConfig implements IUserConfig {
/**
* will change referenced $value with the decrypted value in case of encrypted (sensitive value)
*
* @param string $userId
* @param string $app
* @param string $key
* @param string $value
* @param array{type: ValueType, flags: int, value: string} $valueDetail
*/
private function decryptSensitiveValue(string $userId, string $app, string $key, string &$value): void {
if (!$this->isFlagged(self::FLAG_SENSITIVE, $this->valueDetails[$userId][$app][$key]['flags'] ?? 0)) {
private function decryptSensitiveValue(string $userId, string $app, string $key, array &$valueDetail): void {
if (!$this->isFlagged(self::FLAG_SENSITIVE, $valueDetail['flags'] ?? 0)) {
return;
}
if (!str_starts_with($value, self::ENCRYPTION_PREFIX)) {
if (!str_starts_with($valueDetail['value'], self::ENCRYPTION_PREFIX)) {
return;
}
try {
$value = $this->crypto->decrypt(substr($value, self::ENCRYPTION_PREFIX_LENGTH));
$valueDetail['value'] = $this->crypto->decrypt(substr($valueDetail['value'], self::ENCRYPTION_PREFIX_LENGTH));
} catch (\Exception $e) {
$this->logger->warning('could not decrypt sensitive value', [
'userId' => $userId,
'app' => $app,
'key' => $key,
'value' => $value,
'value' => $valueDetail['value'],
'exception' => $e
]);
}

@ -1423,8 +1423,8 @@ class UserConfigTest extends TestCase {
$this->assertEquals($sensitive, $userConfig->isSensitive($userId, $app, $key));
if ($sensitive) {
$this->assertEquals(true, str_starts_with(
$userConfig->statusCache()['fastCache'][$userId][$app][$key]
?? $userConfig->statusCache()['lazyCache'][$userId][$app][$key],
$userConfig->statusCache()['fastCache'][$userId][$app][$key]['value']
?? $userConfig->statusCache()['lazyCache'][$userId][$app][$key]['value'],
'$UserConfigEncryption$')
);
}
@ -1452,8 +1452,8 @@ class UserConfigTest extends TestCase {
$userConfig->getValueString($userId, $app, $key); // cache loading for userId
$this->assertEquals(
!$sensitive, str_starts_with(
$userConfig->statusCache()['fastCache'][$userId][$app][$key]
?? $userConfig->statusCache()['lazyCache'][$userId][$app][$key],
$userConfig->statusCache()['fastCache'][$userId][$app][$key]['value']
?? $userConfig->statusCache()['lazyCache'][$userId][$app][$key]['value'],
'$UserConfigEncryption$'
)
);
@ -1466,8 +1466,8 @@ class UserConfigTest extends TestCase {
$this->assertEquals($sensitive, $userConfig->isSensitive($userId, $app, $key));
// should only work if updateGlobalSensitive drop cache
$this->assertEquals($sensitive, str_starts_with(
$userConfig->statusCache()['fastCache'][$userId][$app][$key]
?? $userConfig->statusCache()['lazyCache'][$userId][$app][$key],
$userConfig->statusCache()['fastCache'][$userId][$app][$key]['value']
?? $userConfig->statusCache()['lazyCache'][$userId][$app][$key]['value'],
'$UserConfigEncryption$')
);
}
@ -1683,7 +1683,6 @@ class UserConfigTest extends TestCase {
[
'fastCache' => [],
'lazyCache' => [],
'valueDetails' => [],
],
$userConfig->statusCache()
);

Loading…
Cancel
Save