fix: use async CLI Apps catalog refresh
Replace the manual thread-based catalog refresh with an asyncio task and async HTTP catalog fetches so the Settings route stays within the async WebUI model.
This commit is contained in:
@@ -461,6 +461,48 @@ class CliAppManager:
|
||||
_write_json(cache_path, {"_cached_at": _now(), "data": fetched})
|
||||
return fetched
|
||||
|
||||
async def _fetch_registry_async(
|
||||
self,
|
||||
url: str,
|
||||
cache_path: Path,
|
||||
*,
|
||||
force_refresh: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
data, cached_at = self._cached_registry(cache_path)
|
||||
if (
|
||||
not force_refresh
|
||||
and data is not None
|
||||
and _now() - cached_at < self.runtime.catalog_ttl_seconds
|
||||
):
|
||||
return data
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client:
|
||||
response = await client.get(url)
|
||||
response.raise_for_status()
|
||||
fetched = response.json()
|
||||
if not isinstance(fetched, dict):
|
||||
raise ValueError("registry response must be an object")
|
||||
except Exception:
|
||||
if data is not None:
|
||||
return data
|
||||
raise
|
||||
|
||||
_write_json(cache_path, {"_cached_at": _now(), "data": fetched})
|
||||
return fetched
|
||||
|
||||
async def refresh_catalog_cache(self, *, force_refresh: bool = False) -> None:
|
||||
for source, url, _raw_base, required in _CATALOG_SOURCES:
|
||||
try:
|
||||
await self._fetch_registry_async(
|
||||
url,
|
||||
self._cache_path(source),
|
||||
force_refresh=force_refresh,
|
||||
)
|
||||
except Exception:
|
||||
if required:
|
||||
raise
|
||||
|
||||
def catalog(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
@@ -22,37 +22,26 @@ _CLI_APP_ATTACHMENT_KEYS = (
|
||||
"brand_color",
|
||||
)
|
||||
_CATALOG_REFRESH_RETRY_SECONDS = 60.0
|
||||
_catalog_refresh_lock = threading.Lock()
|
||||
_catalog_refresh_running = False
|
||||
_catalog_refresh_task: asyncio.Task[None] | None = None
|
||||
_catalog_refresh_last_started = 0.0
|
||||
|
||||
|
||||
def _start_catalog_refresh() -> bool:
|
||||
global _catalog_refresh_last_started, _catalog_refresh_running
|
||||
async def _refresh_catalog(manager: CliAppManager) -> None:
|
||||
try:
|
||||
await manager.refresh_catalog_cache(force_refresh=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _start_catalog_refresh(manager: CliAppManager) -> bool:
|
||||
global _catalog_refresh_last_started, _catalog_refresh_task
|
||||
now = time.monotonic()
|
||||
with _catalog_refresh_lock:
|
||||
if _catalog_refresh_running:
|
||||
return True
|
||||
if now - _catalog_refresh_last_started < _CATALOG_REFRESH_RETRY_SECONDS:
|
||||
return False
|
||||
_catalog_refresh_running = True
|
||||
_catalog_refresh_last_started = now
|
||||
|
||||
def refresh() -> None:
|
||||
global _catalog_refresh_running
|
||||
try:
|
||||
_manager().catalog(force_refresh=True)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
with _catalog_refresh_lock:
|
||||
_catalog_refresh_running = False
|
||||
|
||||
threading.Thread(
|
||||
target=refresh,
|
||||
name="nanobot-cli-app-catalog-refresh",
|
||||
daemon=True,
|
||||
).start()
|
||||
if _catalog_refresh_task is not None and not _catalog_refresh_task.done():
|
||||
return True
|
||||
if now - _catalog_refresh_last_started < _CATALOG_REFRESH_RETRY_SECONDS:
|
||||
return False
|
||||
_catalog_refresh_last_started = now
|
||||
_catalog_refresh_task = asyncio.create_task(_refresh_catalog(manager))
|
||||
return True
|
||||
|
||||
|
||||
@@ -108,14 +97,14 @@ def _manager() -> CliAppManager:
|
||||
)
|
||||
|
||||
|
||||
def cli_apps_payload(*, installed_only: bool = False) -> dict[str, Any]:
|
||||
async def cli_apps_payload(*, installed_only: bool = False) -> dict[str, Any]:
|
||||
manager = _manager()
|
||||
if installed_only:
|
||||
return manager.installed_payload()
|
||||
payload = manager.payload(cache_only=True)
|
||||
refresh_pending = False
|
||||
if not manager.catalog_cache_fresh(include_optional=True):
|
||||
refresh_pending = _start_catalog_refresh()
|
||||
refresh_pending = _start_catalog_refresh(manager)
|
||||
if not payload["apps"]:
|
||||
installed = manager.installed_payload()
|
||||
if installed["apps"]:
|
||||
|
||||
@@ -8,6 +8,7 @@ request mapping and response shaping.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import json
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
@@ -309,10 +310,12 @@ class WebUISettingsRouter:
|
||||
"yes",
|
||||
}
|
||||
try:
|
||||
if installed_only:
|
||||
payload = await asyncio.to_thread(cli_apps_payload, installed_only=True)
|
||||
else:
|
||||
payload = await asyncio.to_thread(cli_apps_payload)
|
||||
payload_result = (
|
||||
cli_apps_payload(installed_only=True)
|
||||
if installed_only
|
||||
else cli_apps_payload()
|
||||
)
|
||||
payload = await payload_result if inspect.isawaitable(payload_result) else payload_result
|
||||
except Exception:
|
||||
self.logger.exception("failed to load CLI Apps payload")
|
||||
return self._error_response(500, "failed to load CLI Apps")
|
||||
|
||||
Reference in New Issue
Block a user