From 4ef1b84fbf1324e284ac0b16cc971aa7cc079ae1 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 5 Mar 2026 11:41:01 -0500 Subject: [PATCH 01/11] docs(AppConfig): update class docblock Signed-off-by: Josh --- lib/private/AppConfig.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index e4da80b94d94d..518d82c61d8ee 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -33,23 +33,23 @@ use Psr\Log\LoggerInterface; /** - * This class provides an easy way for apps to store config values in the - * database. + * Stores and retrieves per-app configuration values in the database, + * with support for type safety and lazy loading. * - * **Note:** since 29.0.0, it supports **lazy loading** + * ### Lazy Loading (since 29.0.0) + * To minimize (unnecessary) memory usage, only non-lazy configuration values are loaded by default. + * Lazy config values are fetched from the database only when specifically requested. * - * ### What is lazy loading ? - * In order to avoid loading useless config values into memory for each request, - * only non-lazy values are now loaded. + * Warning: When a lazy config value is requested, all lazy config values for that specific app + * are loaded into memory. * - * Once a value that is lazy is requested, all lazy values will be loaded. - * - * Similarly, some methods from this class are marked with a warning about ignoring - * lazy loading. Use them wisely and only on parts of the code that are called - * during specific requests or actions to avoid loading the lazy values all the time. + * Note: Some methods (such as `getKeys()` or `getAllValues()`) bypass lazy loading and will + * forcibly load all lazy config values for the app. + * Use these methods carefully: they should only be called in code paths that run as part of + * specific actions (like admin pages or background jobs), not on every user request. * * @since 7.0.0 - * @since 29.0.0 - Supporting types and lazy loading + * @since 29.0.0 Added support for type safety and lazy loading. */ class AppConfig implements IAppConfig { private const APP_MAX_LENGTH = 32; From f2174caf053138fb96ad2810f7aad7ef4f074fb9 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 6 Mar 2026 09:58:47 -0500 Subject: [PATCH 02/11] docs(AppConfig): add some clarifying comments to loadConfig Signed-off-by: Josh --- lib/private/AppConfig.php | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 518d82c61d8ee..0d89344adfde7 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -1314,23 +1314,33 @@ private function assertParams(string $app = '', string $configKey = '', bool $al } /** - * Load normal config or config set as lazy loaded + * Ensures app config is loaded into in-memory caches. * - * @param bool $lazy set to TRUE to also load config values set as lazy loaded + * Reads from local cache when available; otherwise queries the database and refreshes local cache. + * + * Behavior: + * - $lazy = false: loads non-lazy config values. + * - $lazy = true: ensures lazy values are loaded; may load both non-lazy and lazy values + * if non-lazy values are not loaded yet. + * + * @param string|null $app App ID used for debug logging when lazy loading is triggered + * @param bool $lazy Whether to ensure lazy values are loaded */ private function loadConfig(?string $app = null, bool $lazy = false): void { if ($this->isLoaded($lazy)) { return; } - // if lazy is null or true, we debug log + // Emit debug context for the caller that triggered lazy loading. 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]); } - $loadLazyOnly = $lazy && $this->isLoaded(); + // If non-lazy config is already loaded, a lazy load can query only lazy rows. + $loadLazyOnly = $this->isLoaded() && $lazy; + // Prefer local cache when it contains the required data subset. /** @var array */ $cacheContent = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? []; $includesLazyValues = !empty($cacheContent) && !empty($cacheContent['lazyCache']); @@ -1345,24 +1355,27 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { return; } - // Otherwise no cache available and we need to fetch from database + // Cache miss (or missing lazy subset): fetch the required rows from DB. $qb = $this->connection->getQueryBuilder(); $qb->from('appconfig') ->select('appid', 'configkey', 'configvalue', 'type'); if ($lazy === false) { + // Non-lazy load path. $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); } else { + // Lazy load path; restrict to lazy rows if non-lazy is already in memory. if ($loadLazyOnly) { $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); } + // Include row laziness so mixed result sets can be routed to the right cache. $qb->addSelect('lazy'); } $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 + // Route each row to the corresponding in-memory cache. if ($lazy && ((int)$row['lazy']) === 1) { $this->lazyCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? ''; } else { @@ -1372,6 +1385,8 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { } $result->closeCursor(); + + // Persist refreshed in-memory caches to local cache. $this->localCache?->set( self::LOCAL_CACHE_KEY, [ From 7686cb69f60f41d917c441f04ff50afdea59d07b Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 6 Mar 2026 10:41:27 -0500 Subject: [PATCH 03/11] refactor(AppConfig): clarify loadConfig variable names Signed-off-by: Josh --- lib/private/AppConfig.php | 40 ++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 0d89344adfde7..e54b52f22b5cc 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -1327,29 +1327,31 @@ private function assertParams(string $app = '', string $configKey = '', bool $al * @param bool $lazy Whether to ensure lazy values are loaded */ private function loadConfig(?string $app = null, bool $lazy = false): void { + // If the relevant config values (based on $lazy) are already cached in memory, + // skip database/cache loading and return immediately for efficiency. if ($this->isLoaded($lazy)) { return; } // Emit debug context for the caller that triggered lazy loading. 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]); + $lazyLoadTriggerException = new \RuntimeException('The loading of lazy AppConfig values have been triggered by app "' . $app . '"'); + $this->logger->debug($lazyLoadTriggerException->getMessage(), ['exception' => $lazyLoadTriggerException, 'app' => $app]); } // If non-lazy config is already loaded, a lazy load can query only lazy rows. - $loadLazyOnly = $this->isLoaded() && $lazy; + $shouldLoadLazyOnly = $this->isLoaded() && $lazy; // Prefer local cache when it contains the required data subset. /** @var array */ - $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']; + $cachedConfig = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? []; + $cachedConfigIncludesLazyValues = !empty($cachedConfig) && !empty($cachedConfig['lazyCache']); + if (!empty($cachedConfig) && (!$lazy || $cachedConfigIncludesLazyValues)) { + $this->valueTypes = $cachedConfig['valueTypes']; + $this->fastCache = $cachedConfig['fastCache']; $this->fastLoaded = !empty($this->fastCache); - if ($includesLazyValues) { - $this->lazyCache = $cacheContent['lazyCache']; + if ($cachedConfigIncludesLazyValues) { + $this->lazyCache = $cachedConfig['lazyCache']; $this->lazyLoaded = !empty($this->lazyCache); } return; @@ -1365,26 +1367,26 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); } else { // Lazy load path; restrict to lazy rows if non-lazy is already in memory. - if ($loadLazyOnly) { + if ($shouldLoadLazyOnly) { $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); } // Include row laziness so mixed result sets can be routed to the right cache. $qb->addSelect('lazy'); } - $result = $qb->executeQuery(); - $rows = $result->fetchAll(); - foreach ($rows as $row) { + $queryResult = $qb->executeQuery(); + $configRows = $queryResult->fetchAll(); + foreach ($configRows as $configRow) { // Route each row to the corresponding in-memory cache. - if ($lazy && ((int)$row['lazy']) === 1) { - $this->lazyCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? ''; + if ($lazy && ((int)$configRow['lazy']) === 1) { + $this->lazyCache[$configRow['appid']][$configRow['configkey']] = $configRow['configvalue'] ?? ''; } else { - $this->fastCache[$row['appid']][$row['configkey']] = $row['configvalue'] ?? ''; + $this->fastCache[$configRow['appid']][$configRow['configkey']] = $configRow['configvalue'] ?? ''; } - $this->valueTypes[$row['appid']][$row['configkey']] = (int)($row['type'] ?? 0); + $this->valueTypes[$configRow['appid']][$configRow['configkey']] = (int)($configRow['type'] ?? 0); } - $result->closeCursor(); + $queryResult->closeCursor(); // Persist refreshed in-memory caches to local cache. $this->localCache?->set( From 1e46c8b89242dbc3673aa41665e69469f3714f80 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 6 Mar 2026 10:54:22 -0500 Subject: [PATCH 04/11] refactor(AppConfig): additional comment clarity Signed-off-by: Josh --- lib/private/AppConfig.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index e54b52f22b5cc..dea56e6a1fc59 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -1316,30 +1316,28 @@ private function assertParams(string $app = '', string $configKey = '', bool $al /** * Ensures app config is loaded into in-memory caches. * - * Reads from local cache when available; otherwise queries the database and refreshes local cache. + * Uses local cache when possible; otherwise reads from DB and refreshes local cache. * * Behavior: - * - $lazy = false: loads non-lazy config values. - * - $lazy = true: ensures lazy values are loaded; may load both non-lazy and lazy values - * if non-lazy values are not loaded yet. + * - $lazy = false: load non-lazy values. + * - $lazy = true: ensure lazy values are loaded; may also load non-lazy values if they're not loaded yet. * * @param string|null $app App ID used for debug logging when lazy loading is triggered * @param bool $lazy Whether to ensure lazy values are loaded */ private function loadConfig(?string $app = null, bool $lazy = false): void { - // If the relevant config values (based on $lazy) are already cached in memory, - // skip database/cache loading and return immediately for efficiency. + // Already loaded for the requested mode; skip. if ($this->isLoaded($lazy)) { return; } - // Emit debug context for the caller that triggered lazy loading. + // Log which app triggered lazy loading and include context to help with optimization follow-up. if ($lazy === true && $app !== null) { $lazyLoadTriggerException = new \RuntimeException('The loading of lazy AppConfig values have been triggered by app "' . $app . '"'); $this->logger->debug($lazyLoadTriggerException->getMessage(), ['exception' => $lazyLoadTriggerException, 'app' => $app]); } - // If non-lazy config is already loaded, a lazy load can query only lazy rows. + // If fast/non-lazy config is already loaded, a lazy load can query only lazy rows. $shouldLoadLazyOnly = $this->isLoaded() && $lazy; // Prefer local cache when it contains the required data subset. @@ -1357,7 +1355,7 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { return; } - // Cache miss (or missing lazy subset): fetch the required rows from DB. + // Cache miss (or missing lazy subset): fetch from DB. $qb = $this->connection->getQueryBuilder(); $qb->from('appconfig') ->select('appid', 'configkey', 'configvalue', 'type'); @@ -1370,7 +1368,7 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { if ($shouldLoadLazyOnly) { $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); } - // Include row laziness so mixed result sets can be routed to the right cache. + // Include laziness, when result set may contain both types, so can be routed to the right cache. $qb->addSelect('lazy'); } From 82379f82767d074f05352b6e71c34976a791ec9a Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 6 Mar 2026 11:14:55 -0500 Subject: [PATCH 05/11] refactor(AppConfig): clarity-only control-flow cleanup for loadConfig Signed-off-by: Josh --- lib/private/AppConfig.php | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index dea56e6a1fc59..86e3db84c6571 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -1343,15 +1343,19 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { // Prefer local cache when it contains the required data subset. /** @var array */ $cachedConfig = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? []; - $cachedConfigIncludesLazyValues = !empty($cachedConfig) && !empty($cachedConfig['lazyCache']); - if (!empty($cachedConfig) && (!$lazy || $cachedConfigIncludesLazyValues)) { + $cachedConfigIncludesLazyValues = !empty($cachedConfig['lazyCache']); + $canHydrateFromLocalCache = !empty($cachedConfig) && (!$lazy || $cachedConfigIncludesLazyValues); + + if ($canHydrateFromLocalCache) { $this->valueTypes = $cachedConfig['valueTypes']; $this->fastCache = $cachedConfig['fastCache']; $this->fastLoaded = !empty($this->fastCache); + if ($cachedConfigIncludesLazyValues) { $this->lazyCache = $cachedConfig['lazyCache']; $this->lazyLoaded = !empty($this->lazyCache); } + return; } @@ -1363,20 +1367,24 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { if ($lazy === false) { // Non-lazy load path. $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); + // Lazy load paths... + } elseif ($shouldLoadLazyOnly) { + // Restrict to lazy rows if non-lazy is already in memory. + $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); + $qb->addSelect('lazy'); } else { - // Lazy load path; restrict to lazy rows if non-lazy is already in memory. - if ($shouldLoadLazyOnly) { - $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); - } // Include laziness, when result set may contain both types, so can be routed to the right cache. $qb->addSelect('lazy'); } $queryResult = $qb->executeQuery(); $configRows = $queryResult->fetchAll(); + foreach ($configRows as $configRow) { - // Route each row to the corresponding in-memory cache. - if ($lazy && ((int)$configRow['lazy']) === 1) { + $isLazyRow = $lazy && ((int)$configRow['lazy']) === 1; + + // Route each config row to the corresponding in-memory cache. + if ($isLazyRow) { $this->lazyCache[$configRow['appid']][$configRow['configkey']] = $configRow['configvalue'] ?? ''; } else { $this->fastCache[$configRow['appid']][$configRow['configkey']] = $configRow['configvalue'] ?? ''; From 01359708838866889a570953e03dcd10c80de8a9 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 6 Mar 2026 11:30:17 -0500 Subject: [PATCH 06/11] refactor(AppConfig): Extract loadConfig cache hydration into helper Signed-off-by: Josh --- lib/private/AppConfig.php | 45 ++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 86e3db84c6571..c64a0c9c37054 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -1333,31 +1333,17 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { // Log which app triggered lazy loading and include context to help with optimization follow-up. if ($lazy === true && $app !== null) { - $lazyLoadTriggerException = new \RuntimeException('The loading of lazy AppConfig values have been triggered by app "' . $app . '"'); + $lazyLoadTriggerException = new \RuntimeException('The loading of lazy AppConfig values has been triggered by app "' . $app . '"'); $this->logger->debug($lazyLoadTriggerException->getMessage(), ['exception' => $lazyLoadTriggerException, 'app' => $app]); } - // If fast/non-lazy config is already loaded, a lazy load can query only lazy rows. - $shouldLoadLazyOnly = $this->isLoaded() && $lazy; - // Prefer local cache when it contains the required data subset. - /** @var array */ - $cachedConfig = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? []; - $cachedConfigIncludesLazyValues = !empty($cachedConfig['lazyCache']); - $canHydrateFromLocalCache = !empty($cachedConfig) && (!$lazy || $cachedConfigIncludesLazyValues); - - if ($canHydrateFromLocalCache) { - $this->valueTypes = $cachedConfig['valueTypes']; - $this->fastCache = $cachedConfig['fastCache']; - $this->fastLoaded = !empty($this->fastCache); - - if ($cachedConfigIncludesLazyValues) { - $this->lazyCache = $cachedConfig['lazyCache']; - $this->lazyLoaded = !empty($this->lazyCache); - } - + if ($this->tryLoadFromLocalCache($lazy)) { return; } + + // If fast/non-lazy config is already loaded, a lazy load can query only lazy rows. + $shouldLoadLazyOnly = $this->isLoaded() && $lazy; // Cache miss (or missing lazy subset): fetch from DB. $qb = $this->connection->getQueryBuilder(); @@ -1409,6 +1395,27 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { $this->lazyLoaded = $lazy; } + private function tryLoadFromLocalCache(bool $lazy): bool { + /** @var array $cachedConfig */ + $cachedConfig = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? []; + $cachedConfigIncludesLazyValues = !empty($cachedConfig) && !empty($cachedConfig['lazyCache']); + + if (empty($cachedConfig) || ($lazy && !$cachedConfigIncludesLazyValues)) { + return false; + } + + $this->valueTypes = $cachedConfig['valueTypes']; + $this->fastCache = $cachedConfig['fastCache']; + $this->fastLoaded = !empty($this->fastCache); + + if ($cachedConfigIncludesLazyValues) { + $this->lazyCache = $cachedConfig['lazyCache']; + $this->lazyLoaded = !empty($this->lazyCache); + } + + return true; + } + /** * @param bool $lazy - If set to true then also check if lazy values are loaded */ From d289aafe96a0090331aabbc690e69f06f8329ca3 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 6 Mar 2026 11:38:16 -0500 Subject: [PATCH 07/11] refactor(AppConfig): Extract loadConfig() query builder setup Signed-off-by: Josh --- lib/private/AppConfig.php | 46 +++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index c64a0c9c37054..418bea1012996 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -1346,22 +1346,7 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { $shouldLoadLazyOnly = $this->isLoaded() && $lazy; // Cache miss (or missing lazy subset): fetch from DB. - $qb = $this->connection->getQueryBuilder(); - $qb->from('appconfig') - ->select('appid', 'configkey', 'configvalue', 'type'); - - if ($lazy === false) { - // Non-lazy load path. - $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); - // Lazy load paths... - } elseif ($shouldLoadLazyOnly) { - // Restrict to lazy rows if non-lazy is already in memory. - $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); - $qb->addSelect('lazy'); - } else { - // Include laziness, when result set may contain both types, so can be routed to the right cache. - $qb->addSelect('lazy'); - } + $qb = $this->buildLoadConfigQuery($lazy, $shouldLoadLazyOnly); $queryResult = $qb->executeQuery(); $configRows = $queryResult->fetchAll(); @@ -1395,6 +1380,11 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { $this->lazyLoaded = $lazy; } + /** + * Hydrate in-memory caches from local cache when it contains the required subset. + * + * @return bool True when hydration succeeded; false when DB load is still required. + */ private function tryLoadFromLocalCache(bool $lazy): bool { /** @var array $cachedConfig */ $cachedConfig = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? []; @@ -1416,6 +1406,30 @@ private function tryLoadFromLocalCache(bool $lazy): bool { return true; } + /** + * Build the appConfig query for lazy/non-lazy loading mode. + */ + private function buildLoadConfigQuery(bool $lazy, bool $shouldLoadLazyOnly): IQueryBuilder { + $qb = $this->connection->getQueryBuilder(); + $qb->from('appconfig') + ->select('appid', 'configkey', 'configvalue', 'type'); + + // Non-lazy load path. + if ($lazy === false) { + $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); + return $qb; + } + + // Restrict to lazy rows if non-lazy is already in memory. + if ($shouldLoadLazyOnly) { + $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); + } + + // Include laziness, when result set may contain both types, so can be routed to the right cache. + $qb->addSelect('lazy'); + return $qb; + } + /** * @param bool $lazy - If set to true then also check if lazy values are loaded */ From 5eac68519f18afb6399b5a791218e5bf3a343e76 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 6 Mar 2026 11:44:20 -0500 Subject: [PATCH 08/11] refactor(AppConfig): use local vars inside cache assignment loop Reduces repeated indexing and improves scanability. Signed-off-by: Josh --- lib/private/AppConfig.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 418bea1012996..c9cd050cef46f 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -1352,15 +1352,21 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { $configRows = $queryResult->fetchAll(); foreach ($configRows as $configRow) { + $appId = $configRow['appid']; + $configKey = $configRow['configkey']; + $configValue = $configRow['configvalue'] ?? ''; + $valueType = (int)($configRow['type'] ?? 0); + $isLazyRow = $lazy && ((int)$configRow['lazy']) === 1; // Route each config row to the corresponding in-memory cache. if ($isLazyRow) { - $this->lazyCache[$configRow['appid']][$configRow['configkey']] = $configRow['configvalue'] ?? ''; + $this->lazyCache[$appId][$configKey] = $configValue; } else { - $this->fastCache[$configRow['appid']][$configRow['configkey']] = $configRow['configvalue'] ?? ''; + $this->fastCache[$appId][$configKey] = $configValue; } - $this->valueTypes[$configRow['appid']][$configRow['configkey']] = (int)($configRow['type'] ?? 0); + + $this->valueTypes[$appId][$configKey] = $valueType; } $queryResult->closeCursor(); From 1fae5cba25ad6fee73c16d83805b85b0819ebf91 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 6 Mar 2026 12:04:33 -0500 Subject: [PATCH 09/11] refactor(AppConfig): defensive local-cache shape check w/ logging Unlikely but.. Signed-off-by: Josh --- lib/private/AppConfig.php | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index c9cd050cef46f..b7e22fd5f651a 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -1350,7 +1350,6 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { $queryResult = $qb->executeQuery(); $configRows = $queryResult->fetchAll(); - foreach ($configRows as $configRow) { $appId = $configRow['appid']; $configKey = $configRow['configkey']; @@ -1394,9 +1393,18 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { private function tryLoadFromLocalCache(bool $lazy): bool { /** @var array $cachedConfig */ $cachedConfig = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? []; - $cachedConfigIncludesLazyValues = !empty($cachedConfig) && !empty($cachedConfig['lazyCache']); - if (empty($cachedConfig) || ($lazy && !$cachedConfigIncludesLazyValues)) { + if ( + empty($cachedConfig) + || !isset($cachedConfig['valueTypes'], $cachedConfig['fastCache']) + || ($lazy && !isset($cachedConfig['lazyCache'])) + ) { + $this->logger->debug('Ignoring malformed local AppConfig cache payload', [ + 'hasValueTypes' => isset($cachedConfig['valueTypes']), + 'hasFastCache' => isset($cachedConfig['fastCache']), + 'hasLazyCache' => isset($cachedConfig['lazyCache']), + 'lazyRequested' => $lazy, + ]); return false; } @@ -1404,6 +1412,8 @@ private function tryLoadFromLocalCache(bool $lazy): bool { $this->fastCache = $cachedConfig['fastCache']; $this->fastLoaded = !empty($this->fastCache); + $cachedConfigIncludesLazyValues = !empty($cachedConfig['lazyCache']); + if ($cachedConfigIncludesLazyValues) { $this->lazyCache = $cachedConfig['lazyCache']; $this->lazyLoaded = !empty($this->lazyCache); @@ -1413,7 +1423,7 @@ private function tryLoadFromLocalCache(bool $lazy): bool { } /** - * Build the appConfig query for lazy/non-lazy loading mode. + * Build the appconfig query for lazy/non-lazy loading mode. */ private function buildLoadConfigQuery(bool $lazy, bool $shouldLoadLazyOnly): IQueryBuilder { $qb = $this->connection->getQueryBuilder(); @@ -1431,7 +1441,7 @@ private function buildLoadConfigQuery(bool $lazy, bool $shouldLoadLazyOnly): IQu $qb->where($qb->expr()->eq('lazy', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); } - // Include laziness, when result set may contain both types, so can be routed to the right cache. + // Include laziness when result set may contain both types, so can be routed to the right cache. $qb->addSelect('lazy'); return $qb; } From f91f92d3409848443c2714eb1365eef0a37519e3 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 6 Mar 2026 12:09:45 -0500 Subject: [PATCH 10/11] refactor(AppConfig): skip logging for a merely empty cache Signed-off-by: Josh --- lib/private/AppConfig.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index b7e22fd5f651a..4d871d998ba2a 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -1394,9 +1394,12 @@ private function tryLoadFromLocalCache(bool $lazy): bool { /** @var array $cachedConfig */ $cachedConfig = $this->localCache?->get(self::LOCAL_CACHE_KEY) ?? []; + if (empty($cachedConfig)) { + return false; + } + if ( - empty($cachedConfig) - || !isset($cachedConfig['valueTypes'], $cachedConfig['fastCache']) + !isset($cachedConfig['valueTypes'], $cachedConfig['fastCache']) || ($lazy && !isset($cachedConfig['lazyCache'])) ) { $this->logger->debug('Ignoring malformed local AppConfig cache payload', [ From 480cea755069f0b8b8fee8e86ee6722cfe5d122a Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 9 Mar 2026 14:53:32 -0400 Subject: [PATCH 11/11] chore: fixup AppConfig.php for php-cs Signed-off-by: Josh --- lib/private/AppConfig.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 4d871d998ba2a..31b24adfe21f9 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -43,9 +43,9 @@ * Warning: When a lazy config value is requested, all lazy config values for that specific app * are loaded into memory. * - * Note: Some methods (such as `getKeys()` or `getAllValues()`) bypass lazy loading and will + * Note: Some methods (such as `getKeys()` or `getAllValues()`) bypass lazy loading and will * forcibly load all lazy config values for the app. - * Use these methods carefully: they should only be called in code paths that run as part of + * Use these methods carefully: they should only be called in code paths that run as part of * specific actions (like admin pages or background jobs), not on every user request. * * @since 7.0.0 @@ -1323,7 +1323,7 @@ private function assertParams(string $app = '', string $configKey = '', bool $al * - $lazy = true: ensure lazy values are loaded; may also load non-lazy values if they're not loaded yet. * * @param string|null $app App ID used for debug logging when lazy loading is triggered - * @param bool $lazy Whether to ensure lazy values are loaded + * @param bool $lazy Whether to ensure lazy values are loaded */ private function loadConfig(?string $app = null, bool $lazy = false): void { // Already loaded for the requested mode; skip. @@ -1341,7 +1341,7 @@ private function loadConfig(?string $app = null, bool $lazy = false): void { if ($this->tryLoadFromLocalCache($lazy)) { return; } - + // If fast/non-lazy config is already loaded, a lazy load can query only lazy rows. $shouldLoadLazyOnly = $this->isLoaded() && $lazy;