From ad268516a9ac55ca921b50fda1a058bf33c2d75e Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 4 Dec 2025 10:57:17 +0200 Subject: [PATCH] [perf] Extension list cache (#3616) --- lnbits/core/models/extensions.py | 33 ++++++++++++++++++++++++++++++ lnbits/core/tasks.py | 4 +++- lnbits/core/views/extension_api.py | 4 +++- lnbits/utils/cache.py | 6 ++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/lnbits/core/models/extensions.py b/lnbits/core/models/extensions.py index d3fa6499..adb0272f 100644 --- a/lnbits/core/models/extensions.py +++ b/lnbits/core/models/extensions.py @@ -6,6 +6,7 @@ import json import os import shutil import zipfile +from asyncio.tasks import create_task from pathlib import Path from typing import Any @@ -20,6 +21,7 @@ from lnbits.helpers import ( version_parse, ) from lnbits.settings import settings +from lnbits.utils.cache import cache class ExplicitRelease(BaseModel): @@ -606,6 +608,37 @@ class InstallableExtension(BaseModel): @classmethod async def get_installable_extensions( + cls, post_refresh_cache: bool = False + ) -> list[InstallableExtension]: + extension_list: list[InstallableExtension] = [] + + cache_key = "extensions:installable" + cache_value = cache.value(cache_key) + if not cache_value: + extension_list = await cls._get_installable_extensions() + cache.set(cache_key, extension_list, expiry=3600) # one hour + return extension_list + + if cache_value.older_than(10 * 60) or post_refresh_cache: + # refresh cache in background if older than 10 minutes or requested + create_task(cls._refresh_installable_extensions_cache()) + + extension_list = cache_value.value # type: ignore + return extension_list + + @classmethod + async def _refresh_installable_extensions_cache( + cls, + ) -> None: + cache_key = "extensions:installable" + extension_list: list[InstallableExtension] = ( + await cls._get_installable_extensions() + ) + + cache.set(cache_key, extension_list, expiry=3600) + + @classmethod + async def _get_installable_extensions( cls, ) -> list[InstallableExtension]: extension_list: list[InstallableExtension] = [] diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index 4fcc0dad..a27756ce 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -57,7 +57,9 @@ async def run_by_the_minute_tasks() -> None: if minute_counter % 60 == 0: try: # initialize the list of all extensions - await InstallableExtension.get_installable_extensions() + await InstallableExtension.get_installable_extensions( + post_refresh_cache=True + ) except Exception as ex: logger.error(ex) diff --git a/lnbits/core/views/extension_api.py b/lnbits/core/views/extension_api.py index c9373635..63a3a3c3 100644 --- a/lnbits/core/views/extension_api.py +++ b/lnbits/core/views/extension_api.py @@ -508,7 +508,9 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)): installed_exts: list[InstallableExtension] = await get_installed_extensions() installed_exts_ids = [e.id for e in installed_exts] - installable_exts = await InstallableExtension.get_installable_extensions() + installable_exts = await InstallableExtension.get_installable_extensions( + post_refresh_cache=account_id.is_admin_id + ) installable_exts_ids = [e.id for e in installable_exts] installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids] diff --git a/lnbits/utils/cache.py b/lnbits/utils/cache.py index aa22c4e7..904447e1 100644 --- a/lnbits/utils/cache.py +++ b/lnbits/utils/cache.py @@ -13,6 +13,9 @@ class Cached(NamedTuple): value: Any expiry: float + def older_than(self, seconds: float) -> bool: + return time() - self.expiry > seconds + class Cache: """ @@ -23,6 +26,9 @@ class Cache: self.interval = interval self._values: dict[Any, Cached] = {} + def value(self, key: str) -> Cached | None: + return self._values.get(key) + def get(self, key: str, default=None) -> Any | None: cached = self._values.get(key) if cached is not None: