From 8826e40d956f7d981d2f3b4f0254151dca26ad25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B2?= Date: Fri, 22 May 2026 02:07:07 +0300 Subject: [PATCH] fix(keenetic): non-blocking refresh-all and longer timeouts Run refresh-all in background so API stays responsive during 24-router poll. Cap per-device poll at 75s, add 2s gap between routers, bump KeenDNS connect/read timeouts. Co-authored-by: Cursor --- server/api/keenetic.py | 65 ++++++++++++++++++++++++------ server/services/keenetic_client.py | 4 +- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/server/api/keenetic.py b/server/api/keenetic.py index 98a976c..6d5503e 100644 --- a/server/api/keenetic.py +++ b/server/api/keenetic.py @@ -22,8 +22,11 @@ router = APIRouter(prefix="/api/keenetic", tags=["keenetic"]) logger = logging.getLogger(__name__) keenetic_metrics: Dict[str, dict] = {} +_refresh_all_running = False KEENETIC_FILE = DATA_DIR / "keenetic.json" +DEVICE_REFRESH_TIMEOUT = 75 +REFRESH_ALL_GAP_SEC = 2 def _load_keenetic(): @@ -73,8 +76,19 @@ async def _refresh_device(dev: dict) -> dict: client = _client_for_device(dev) try: cached = keenetic_metrics.get(name) - metrics = await client.collect_metrics(cached_info=cached) - metrics["last_updated"] = datetime.now().isoformat() + try: + metrics = await asyncio.wait_for( + client.collect_metrics(cached_info=cached), + timeout=DEVICE_REFRESH_TIMEOUT, + ) + except asyncio.TimeoutError: + metrics = { + "online": False, + "error": "Poll timeout", + "last_updated": datetime.now().isoformat(), + } + else: + metrics["last_updated"] = datetime.now().isoformat() keenetic_metrics[name] = metrics return metrics finally: @@ -283,18 +297,45 @@ async def keenetic_refresh(name: str, request: Request, user: str = Depends(requ return {"status": "error", "detail": str(e)} -@router.post("/refresh-all") -async def keenetic_refresh_all(request: Request, user: str = Depends(require_auth)): +async def _refresh_all_devices() -> list: + global _refresh_all_running devices = _load_keenetic() results = [] - for dev in devices: - try: - metrics = await _refresh_device(dev) - results.append({"name": dev["name"], "online": metrics["online"], - "error": metrics.get("error", "")}) - except Exception as e: - results.append({"name": dev["name"], "online": False, "error": str(e)}) - return {"status": "ok", "results": results} + try: + for i, dev in enumerate(devices): + try: + metrics = await _refresh_device(dev) + results.append({ + "name": dev["name"], + "online": metrics["online"], + "error": metrics.get("error", ""), + }) + except Exception as e: + results.append({"name": dev["name"], "online": False, "error": str(e)}) + if i + 1 < len(devices): + await asyncio.sleep(REFRESH_ALL_GAP_SEC) + finally: + _refresh_all_running = False + return results + + +@router.post("/refresh-all") +async def keenetic_refresh_all(request: Request, user: str = Depends(require_auth)): + global _refresh_all_running + if _refresh_all_running: + return {"status": "ok", "message": "refresh already running"} + + devices = _load_keenetic() + if not devices: + return {"status": "ok", "results": []} + + _refresh_all_running = True + asyncio.create_task(_refresh_all_devices()) + return { + "status": "ok", + "message": f"refresh started for {len(devices)} routers", + "count": len(devices), + } @router.get("/detail/{name}") diff --git a/server/services/keenetic_client.py b/server/services/keenetic_client.py index 42586f9..5c494aa 100644 --- a/server/services/keenetic_client.py +++ b/server/services/keenetic_client.py @@ -60,7 +60,7 @@ def client_timeout_for(host: str) -> aiohttp.ClientTimeout: """Timeouts tuned for KeenDNS vs direct IP.""" if is_public_ip_host(host): return aiohttp.ClientTimeout(total=20, connect=8, sock_read=12) - return aiohttp.ClientTimeout(total=45, connect=15, sock_read=30) + return aiohttp.ClientTimeout(total=60, connect=20, sock_read=40) def build_api_base_url(host: str, web_url: str = "") -> str: @@ -214,7 +214,7 @@ class KeeneticClient: ) await self._reset_session() if attempt + 1 < AUTH_RETRIES: - await asyncio.sleep(1.5 * (attempt + 1)) + await asyncio.sleep(2.0 * (attempt + 1)) continue logger.error(f"Keenetic auth timeout @ {self.base_url}") return False