fix(config): keep the category cache in the memory cache when available

A cache belongs in Nextcloud's memory cache rather than in the app
config. Use the distributed cache (e.g. Redis) with a one-week TTL when
one is available; Nextcloud falls back to the local cache (APCu) when
only that is configured. Without any memory cache the lazy app config
value stays as the fallback. A leftover app config value is removed
when the categories are stored in the memory cache, and saving the
settings clears both.
This commit is contained in:
2026-09-30 13:26:45 +02:00
parent 22e87e12ad
commit 73dea84d89
2 changed files with 54 additions and 16 deletions
+51 -15
View File
@@ -6,6 +6,8 @@ use OC\App\AppStore\Fetcher\CategoryFetcher;
use OCA\SideMenu\AppInfo\Application;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IAppConfig;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IUserSession;
use OCP\L10N\IFactory;
@@ -33,6 +35,7 @@ class CategoryRepository
protected IAppConfig $appConfig,
protected ITimeFactory $timeFactory,
protected LangRepository $langRepository,
protected ICacheFactory $cacheFactory,
) {
}
@@ -94,6 +97,7 @@ class CategoryRepository
public function clearCache(): void
{
$this->getDistributedCache()?->remove(self::CACHE_KEY);
$this->appConfig->deleteKey(Application::APP_ID, self::CACHE_KEY);
}
@@ -117,18 +121,52 @@ class CategoryRepository
}
/**
* Returns the app store categories with only their names, from a
* cache that is stored lazily: it holds the names in every language,
* so it must not be loaded on every request with the other settings.
* Returns the app store categories with only their names. They are
* cached in the distributed cache (e.g. Redis) when one is configured,
* otherwise in a lazy app config value, so they are not loaded on
* every request with the other settings.
*/
protected function getCategories(): array
{
$cache = $this->appConfig->getValueArray(Application::APP_ID, self::CACHE_KEY, [], lazy: true);
$cache = $this->getDistributedCache();
if (isset($cache['expires'], $cache['categories']) && $cache['expires'] > $this->timeFactory->getTime()) {
return $cache['categories'];
if ($cache !== null) {
$categories = $cache->get(self::CACHE_KEY);
if (is_array($categories)) {
return $categories;
}
} else {
$stored = $this->appConfig->getValueArray(Application::APP_ID, self::CACHE_KEY, [], lazy: true);
if (isset($stored['expires'], $stored['categories']) && $stored['expires'] > $this->timeFactory->getTime()) {
return $stored['categories'];
}
}
$categories = $this->fetchCategories();
// Don't cache an empty list, e.g. when the app store is unreachable
if (empty($categories)) {
return $categories;
}
$this->clearCache();
if ($cache !== null) {
$cache->set(self::CACHE_KEY, $categories, self::CACHE_TTL);
} else {
$this->appConfig->setValueArray(Application::APP_ID, self::CACHE_KEY, [
'expires' => $this->timeFactory->getTime() + self::CACHE_TTL,
'categories' => $categories,
], lazy: true);
}
return $categories;
}
protected function fetchCategories(): array
{
$langs = $this->getCachedLangs();
$categories = [];
@@ -147,15 +185,13 @@ class CategoryRepository
];
}
// Don't cache an empty list, e.g. when the app store is unreachable
if (!empty($categories)) {
$this->clearCache();
$this->appConfig->setValueArray(Application::APP_ID, self::CACHE_KEY, [
'expires' => $this->timeFactory->getTime() + self::CACHE_TTL,
'categories' => $categories,
], lazy: true);
}
return $categories;
}
protected function getDistributedCache(): ?ICache
{
return $this->cacheFactory->isAvailable()
? $this->cacheFactory->createDistributed(Application::APP_ID)
: null;
}
}