diff --git a/.env.example b/.env.example index 954deea4..18c43884 100644 --- a/.env.example +++ b/.env.example @@ -141,7 +141,6 @@ POLYWEATHER_SCAN_AI_MAX_ROWS=40 POLYWEATHER_SCAN_AI_MAX_TOKENS=3200 POLYWEATHER_SCAN_CITY_AI_MAX_TOKENS=900 POLYWEATHER_SCAN_AI_PROXY_TIMEOUT_MS=55000 -POLYWEATHER_PREWARM_CITIES=ankara,istanbul,shanghai,beijing,shenzhen,guangzhou,wuhan,chengdu,chongqing,hong kong,taipei,singapore,tokyo,seoul,busan,london,paris,madrid POLYWEATHER_CITY_SUMMARY_CACHE_TTL_SEC=1800 POLYWEATHER_CITY_PANEL_CACHE_TTL_SEC=1800 POLYWEATHER_CITY_NEARBY_CACHE_TTL_SEC=1800 diff --git a/docker-compose.yml b/docker-compose.yml index 648a6380..36a659a1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,14 +34,3 @@ services: # UID/GID are mainly useful on Linux hosts to avoid root-owned output files. user: "${UID:-1000}:${GID:-1000}" - polyweather_prewarm: - <<: *polyweather-base - container_name: polyweather_prewarm - restart: unless-stopped - profiles: ["workers"] - command: python scripts/prewarm_dashboard_worker.py --include-detail --include-market - volumes: - - ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather - - ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data - user: "${UID:-1000}:${GID:-1000}" - diff --git a/scripts/prewarm_dashboard_cache.py b/scripts/prewarm_dashboard_cache.py deleted file mode 100644 index e540bf65..00000000 --- a/scripts/prewarm_dashboard_cache.py +++ /dev/null @@ -1,49 +0,0 @@ -from __future__ import annotations - -import argparse -import os -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - - -def parse_args() -> argparse.Namespace: - from src.utils.prewarm_dashboard import DEFAULT_CITIES - - parser = argparse.ArgumentParser( - description="Prewarm PolyWeather summary/detail/market caches for selected cities.", - ) - parser.add_argument( - "--base-url", - default=os.getenv("POLYWEATHER_BACKEND_URL", "http://127.0.0.1:8000"), - help="Backend base URL, defaults to POLYWEATHER_BACKEND_URL or http://127.0.0.1:8000", - ) - parser.add_argument( - "--cities", - default=",".join(DEFAULT_CITIES), - help="Comma-separated city names to prewarm", - ) - parser.add_argument("--force-refresh", action="store_true") - parser.add_argument("--include-detail", action="store_true") - parser.add_argument("--include-market", action="store_true") - return parser.parse_args() - - -def main() -> int: - args = parse_args() - from src.utils.prewarm_dashboard import run_prewarm - - return run_prewarm( - base_url=args.base_url, - cities=args.cities, - force_refresh=bool(args.force_refresh), - include_detail=bool(args.include_detail), - include_market=bool(args.include_market), - ) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/prewarm_dashboard_worker.py b/scripts/prewarm_dashboard_worker.py deleted file mode 100644 index 25019cfc..00000000 --- a/scripts/prewarm_dashboard_worker.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -import argparse -import os -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - - -def parse_args() -> argparse.Namespace: - from src.utils.prewarm_dashboard import DEFAULT_CITIES - - parser = argparse.ArgumentParser( - description="Run a background dashboard prewarm worker for hot PolyWeather cities.", - ) - parser.add_argument( - "--base-url", - default=os.getenv("POLYWEATHER_BACKEND_URL", "http://127.0.0.1:8000"), - help="Backend base URL, defaults to POLYWEATHER_BACKEND_URL or http://127.0.0.1:8000", - ) - parser.add_argument( - "--cities", - default=",".join(DEFAULT_CITIES), - help="Comma-separated city names to prewarm", - ) - parser.add_argument( - "--interval-sec", - type=int, - default=int(os.getenv("POLYWEATHER_PREWARM_INTERVAL_SEC", "300")), - help="Worker interval in seconds", - ) - parser.add_argument( - "--jitter-sec", - type=int, - default=int(os.getenv("POLYWEATHER_PREWARM_JITTER_SEC", "20")), - help="Random jitter added to each loop in seconds", - ) - parser.add_argument("--force-refresh", action="store_true") - parser.add_argument("--include-detail", action="store_true") - parser.add_argument("--include-market", action="store_true") - parser.add_argument("--once", action="store_true") - return parser.parse_args() - - -def main() -> int: - args = parse_args() - from src.utils.prewarm_dashboard import run_worker_loop - - return run_worker_loop( - base_url=args.base_url, - cities=args.cities, - interval_sec=args.interval_sec, - jitter_sec=args.jitter_sec, - force_refresh=bool(args.force_refresh), - include_detail=bool(args.include_detail), - include_market=bool(args.include_market), - once=bool(args.once), - ) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/bot/runtime_coordinator.py b/src/bot/runtime_coordinator.py index d0457d7f..c8065aa6 100644 --- a/src/bot/runtime_coordinator.py +++ b/src/bot/runtime_coordinator.py @@ -84,7 +84,6 @@ class StartupCoordinator: def start_all(self) -> RuntimeStatus: loops = [ self._start_airport_high_freq_loop(), - self._start_dashboard_prewarm_loop(), self._start_polygon_wallet_loop(), self._start_polymarket_wallet_activity_loop(), self._start_weekly_reward_loop(), @@ -175,32 +174,6 @@ class StartupCoordinator: ), ) - def _start_dashboard_prewarm_loop(self) -> LoopStatus: - enabled = _env_bool("POLYWEATHER_DASHBOARD_PREWARM_ENABLED", False) - interval = max(30, _env_int("POLYWEATHER_PREWARM_INTERVAL_SEC", 300)) - jitter = max(0, _env_int("POLYWEATHER_PREWARM_JITTER_SEC", 20)) - cities_count = _parse_csv_count(os.getenv("POLYWEATHER_PREWARM_CITIES")) or 14 - details = { - "interval_sec": interval, - "jitter_sec": jitter, - "cities_count": cities_count, - "include_detail": _env_bool("POLYWEATHER_PREWARM_INCLUDE_DETAIL", True), - "include_market": _env_bool("POLYWEATHER_PREWARM_INCLUDE_MARKET", True), - "force_refresh": _env_bool("POLYWEATHER_PREWARM_FORCE_REFRESH", False), - "base_url": str(os.getenv("POLYWEATHER_BACKEND_URL") or "http://127.0.0.1:8000").strip(), - } - validation_error = None - if not str(os.getenv("POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN") or "").strip(): - validation_error = "missing_POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN" - return self._start_with_validation( - key="dashboard_prewarm", - label="站点面板预热", - configured_enabled=enabled, - details=details, - validation_error=validation_error, - starter=lambda: import_module("src.utils.prewarm_dashboard").start_prewarm_worker_thread(), - ) - def _start_polygon_wallet_loop(self) -> LoopStatus: enabled = _env_bool("POLYGON_WALLET_WATCH_ENABLED", False) chat_ids = get_telegram_chat_ids_from_env() diff --git a/src/utils/prewarm_dashboard.py b/src/utils/prewarm_dashboard.py deleted file mode 100644 index d309a500..00000000 --- a/src/utils/prewarm_dashboard.py +++ /dev/null @@ -1,418 +0,0 @@ -from __future__ import annotations - -import json -import os -import random -import threading -import time -from datetime import datetime -from typing import Any, Dict, Optional - -import httpx -from loguru import logger - -from src.database.db_manager import DBManager - - -DEFAULT_CITIES = [ - "ankara", - "istanbul", - "shanghai", - "beijing", - "shenzhen", - "guangzhou", - "qingdao", - "wuhan", - "chengdu", - "chongqing", - "hong kong", - "taipei", - "singapore", - "tokyo", - "seoul", - "busan", - "london", - "paris", - "madrid", - "amsterdam", - "helsinki", - "lau fau shan", - "new york", - "los angeles", - "chicago", - "denver", - "atlanta", - "miami", - "san francisco", - "houston", - "dallas", - "austin", - "seattle", -] - -_RUNTIME_LOCK = threading.Lock() -_WORKER_THREAD: Optional[threading.Thread] = None -_DB = DBManager() -_RUNTIME_STATE_KEY = "dashboard_prewarm" -_RUNTIME_STATE: Dict[str, Any] = { - "cycle_count": 0, - "success_count": 0, - "failure_count": 0, - "last_started_at": None, - "last_finished_at": None, - "last_duration_sec": None, - "last_success": None, - "last_http_status": None, - "last_error": None, - "last_requested_cities": [], - "last_requested_city_count": 0, - "last_include_detail": False, - "last_include_market": False, - "last_force_refresh": False, - "last_warmed_count": 0, - "last_summary_ok": 0, - "last_detail_ok": 0, - "last_market_ok": 0, - "last_failed_count": 0, - "last_heartbeat_ts": None, - "writer_mode": None, - "writer_pid": None, - "writer_thread_name": None, -} - - -def _truthy_env(value: str, default: bool = False) -> bool: - raw = str(os.getenv(value) or "").strip().lower() - if not raw: - return default - return raw in {"1", "true", "yes", "on"} - - -def _parse_cities(value: str) -> list[str]: - items = [item.strip() for item in str(value or "").split(",")] - return [item for item in items if item] - - -def _update_runtime_state(**kwargs: Any) -> None: - with _RUNTIME_LOCK: - _RUNTIME_STATE.update(kwargs) - - -def _runtime_mode() -> str: - current = threading.current_thread() - if current.name == "dashboard-prewarm-worker": - return "embedded_thread" - if str(os.getenv("POLYWEATHER_PREWARM_WORKER_MODE") or "").strip(): - return str(os.getenv("POLYWEATHER_PREWARM_WORKER_MODE") or "").strip().lower() - return "standalone_process" - - -def _snapshot_runtime_state() -> Dict[str, Any]: - with _RUNTIME_LOCK: - snapshot = dict(_RUNTIME_STATE) - snapshot["last_heartbeat_ts"] = time.time() - snapshot["writer_mode"] = _runtime_mode() - snapshot["writer_pid"] = os.getpid() - snapshot["writer_thread_name"] = threading.current_thread().name - return snapshot - - -def _persist_runtime_state() -> Dict[str, Any]: - snapshot = _snapshot_runtime_state() - with _RUNTIME_LOCK: - _RUNTIME_STATE.update( - { - "last_heartbeat_ts": snapshot["last_heartbeat_ts"], - "writer_mode": snapshot["writer_mode"], - "writer_pid": snapshot["writer_pid"], - "writer_thread_name": snapshot["writer_thread_name"], - } - ) - try: - _DB.set_payment_runtime_state(_RUNTIME_STATE_KEY, snapshot) - except Exception as exc: - logger.debug("dashboard prewarm runtime persist failed: {}", exc) - return snapshot - - -def _record_prewarm_result( - *, - ok: bool, - duration_sec: float, - http_status: Optional[int], - error: Optional[str], - warmed_count: int, - summary_ok: int, - detail_ok: int, - market_ok: int, - failed_count: int, -) -> None: - with _RUNTIME_LOCK: - _RUNTIME_STATE["cycle_count"] = int(_RUNTIME_STATE.get("cycle_count") or 0) + 1 - if ok: - _RUNTIME_STATE["success_count"] = int(_RUNTIME_STATE.get("success_count") or 0) + 1 - else: - _RUNTIME_STATE["failure_count"] = int(_RUNTIME_STATE.get("failure_count") or 0) + 1 - _RUNTIME_STATE["last_finished_at"] = datetime.now().isoformat(timespec="seconds") - _RUNTIME_STATE["last_duration_sec"] = round(float(duration_sec), 2) - _RUNTIME_STATE["last_success"] = bool(ok) - _RUNTIME_STATE["last_http_status"] = http_status - _RUNTIME_STATE["last_error"] = error - _RUNTIME_STATE["last_warmed_count"] = int(warmed_count or 0) - _RUNTIME_STATE["last_summary_ok"] = int(summary_ok or 0) - _RUNTIME_STATE["last_detail_ok"] = int(detail_ok or 0) - _RUNTIME_STATE["last_market_ok"] = int(market_ok or 0) - _RUNTIME_STATE["last_failed_count"] = int(failed_count or 0) - _persist_runtime_state() - - -def _parse_iso_timestamp(value: Any) -> float: - text = str(value or "").strip() - if not text: - return 0.0 - try: - return datetime.fromisoformat(text).timestamp() - except Exception: - return 0.0 - - -def _runtime_sort_key(payload: Dict[str, Any]) -> float: - if not isinstance(payload, dict): - return 0.0 - candidates = [ - float(payload.get("last_heartbeat_ts") or 0.0), - _parse_iso_timestamp(payload.get("last_finished_at")), - _parse_iso_timestamp(payload.get("last_started_at")), - ] - return max(candidates) - - -def _load_shared_runtime_state() -> Dict[str, Any]: - try: - payload = _DB.get_payment_runtime_state(_RUNTIME_STATE_KEY) - except Exception as exc: - logger.debug("dashboard prewarm runtime load failed: {}", exc) - return {} - return payload if isinstance(payload, dict) else {} - - -def get_prewarm_runtime_summary() -> Dict[str, Any]: - configured_cities = _parse_cities(str(os.getenv("POLYWEATHER_PREWARM_CITIES") or ",".join(DEFAULT_CITIES))) - with _RUNTIME_LOCK: - local_runtime = dict(_RUNTIME_STATE) - shared_runtime = _load_shared_runtime_state() - runtime = local_runtime - if _runtime_sort_key(shared_runtime) > _runtime_sort_key(local_runtime): - runtime = shared_runtime - interval_sec = max(30, int(os.getenv("POLYWEATHER_PREWARM_INTERVAL_SEC", "300"))) - jitter_sec = max(0, int(os.getenv("POLYWEATHER_PREWARM_JITTER_SEC", "20"))) - heartbeat_age_sec = None - last_heartbeat_ts = float(runtime.get("last_heartbeat_ts") or 0.0) - if last_heartbeat_ts > 0: - heartbeat_age_sec = max(0.0, time.time() - last_heartbeat_ts) - shared_alive = bool( - last_heartbeat_ts > 0 - and heartbeat_age_sec is not None - and heartbeat_age_sec <= float(interval_sec + jitter_sec + 90) - ) - return { - "enabled": _truthy_env("POLYWEATHER_DASHBOARD_PREWARM_ENABLED", False), - "base_url": str(os.getenv("POLYWEATHER_BACKEND_URL") or "http://127.0.0.1:8000").strip(), - "configured_cities": configured_cities, - "configured_city_count": len(configured_cities), - "interval_sec": interval_sec, - "jitter_sec": jitter_sec, - "include_detail": _truthy_env("POLYWEATHER_PREWARM_INCLUDE_DETAIL", True), - "include_market": _truthy_env("POLYWEATHER_PREWARM_INCLUDE_MARKET", False), - "force_refresh": _truthy_env("POLYWEATHER_PREWARM_FORCE_REFRESH", False), - "thread_alive": bool(_WORKER_THREAD and _WORKER_THREAD.is_alive()) or shared_alive, - "heartbeat_age_sec": None if heartbeat_age_sec is None else round(heartbeat_age_sec, 2), - "runtime": runtime, - } - - -def run_prewarm( - *, - base_url: str, - cities: str, - force_refresh: bool, - include_detail: bool, - include_market: bool, -) -> int: - token = str(os.getenv("POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN") or "").strip() - requested_cities = _parse_cities(cities) - started = time.perf_counter() - _update_runtime_state( - last_started_at=datetime.now().isoformat(timespec="seconds"), - last_requested_cities=requested_cities, - last_requested_city_count=len(requested_cities), - last_include_detail=bool(include_detail), - last_include_market=bool(include_market), - last_force_refresh=bool(force_refresh), - last_error=None, - last_http_status=None, - ) - _persist_runtime_state() - if not token: - _record_prewarm_result( - ok=False, - duration_sec=time.perf_counter() - started, - http_status=None, - error="missing_backend_token", - warmed_count=0, - summary_ok=0, - detail_ok=0, - market_ok=0, - failed_count=0, - ) - print( - json.dumps( - { - "ok": False, - "reason": "missing_backend_token", - "detail": "POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN is required", - }, - ensure_ascii=False, - ) - ) - return 1 - - try: - with httpx.Client(timeout=180, follow_redirects=True) as client: - response = client.post( - f"{base_url.rstrip('/')}/api/system/prewarm", - params={ - "cities": cities, - "force_refresh": str(bool(force_refresh)).lower(), - "include_detail": str(bool(include_detail)).lower(), - "include_market": str(bool(include_market)).lower(), - }, - headers={ - "Accept": "application/json", - "x-polyweather-entitlement": token, - }, - ) - try: - payload = response.json() - except Exception: - payload = {"ok": False, "raw": (response.text or "")[:500]} - warmed_count = int((payload or {}).get("count") or 0) if isinstance(payload, dict) else 0 - summary_ok = int((payload or {}).get("summary_ok") or 0) if isinstance(payload, dict) else 0 - detail_ok = int((payload or {}).get("detail_ok") or 0) if isinstance(payload, dict) else 0 - market_ok = int((payload or {}).get("market_ok") or 0) if isinstance(payload, dict) else 0 - failed_count = int((payload or {}).get("failed_count") or 0) if isinstance(payload, dict) else 0 - ok = bool(response.is_success and (not isinstance(payload, dict) or payload.get("ok", True))) - _record_prewarm_result( - ok=ok, - duration_sec=time.perf_counter() - started, - http_status=response.status_code, - error=None if ok else str((payload or {}).get("detail") or (payload or {}).get("reason") or response.text[:200]), - warmed_count=warmed_count, - summary_ok=summary_ok, - detail_ok=detail_ok, - market_ok=market_ok, - failed_count=failed_count, - ) - print(json.dumps(payload, ensure_ascii=False, indent=2)) - return 0 if response.is_success else 1 - except Exception as exc: - _record_prewarm_result( - ok=False, - duration_sec=time.perf_counter() - started, - http_status=None, - error=str(exc), - warmed_count=0, - summary_ok=0, - detail_ok=0, - market_ok=0, - failed_count=0, - ) - print(json.dumps({"ok": False, "reason": "request_failed", "detail": str(exc)}, ensure_ascii=False, indent=2)) - return 1 - - -def run_worker_loop( - *, - base_url: str, - cities: str, - interval_sec: int, - jitter_sec: int, - force_refresh: bool, - include_detail: bool, - include_market: bool, - once: bool = False, -) -> int: - interval_sec = max(30, int(interval_sec)) - jitter_sec = max(0, int(jitter_sec)) - - logger.info( - "dashboard prewarm worker started base_url={} cities={} interval_sec={} jitter_sec={} include_detail={} include_market={} force_refresh={} once={}", - base_url, - cities, - interval_sec, - jitter_sec, - bool(include_detail), - bool(include_market), - bool(force_refresh), - bool(once), - ) - _persist_runtime_state() - - while True: - started = time.perf_counter() - exit_code = run_prewarm( - base_url=base_url, - cities=cities, - force_refresh=bool(force_refresh), - include_detail=bool(include_detail), - include_market=bool(include_market), - ) - elapsed = time.perf_counter() - started - logger.info( - "dashboard prewarm worker cycle exit_code={} elapsed_sec={} finished_at={}", - exit_code, - round(elapsed, 2), - datetime.now().isoformat(timespec="seconds"), - ) - if once: - return exit_code - - sleep_sec = max(5.0, interval_sec - elapsed) - if jitter_sec > 0: - sleep_sec += random.randint(0, jitter_sec) - logger.info("dashboard prewarm worker sleeping sleep_sec={}", round(sleep_sec, 2)) - time.sleep(sleep_sec) - - -def start_prewarm_worker_thread() -> Optional[threading.Thread]: - enabled = str(os.getenv("POLYWEATHER_DASHBOARD_PREWARM_ENABLED") or "").strip().lower() - if enabled not in {"1", "true", "yes", "on"}: - return None - - base_url = str(os.getenv("POLYWEATHER_BACKEND_URL") or "http://127.0.0.1:8000").strip() - cities = str(os.getenv("POLYWEATHER_PREWARM_CITIES") or ",".join(DEFAULT_CITIES)).strip() - interval_sec = int(os.getenv("POLYWEATHER_PREWARM_INTERVAL_SEC", "300")) - jitter_sec = int(os.getenv("POLYWEATHER_PREWARM_JITTER_SEC", "20")) - force_refresh = str(os.getenv("POLYWEATHER_PREWARM_FORCE_REFRESH") or "").strip().lower() in {"1", "true", "yes", "on"} - include_detail = str(os.getenv("POLYWEATHER_PREWARM_INCLUDE_DETAIL", "true")).strip().lower() in {"1", "true", "yes", "on"} - include_market = str(os.getenv("POLYWEATHER_PREWARM_INCLUDE_MARKET", "false")).strip().lower() in {"1", "true", "yes", "on"} - - thread = threading.Thread( - target=run_worker_loop, - kwargs={ - "base_url": base_url, - "cities": cities, - "interval_sec": interval_sec, - "jitter_sec": jitter_sec, - "force_refresh": force_refresh, - "include_detail": include_detail, - "include_market": include_market, - "once": False, - }, - name="dashboard-prewarm-worker", - daemon=True, - ) - thread.start() - global _WORKER_THREAD - _WORKER_THREAD = thread - return thread diff --git a/tests/test_web_observability.py b/tests/test_web_observability.py index 426550cc..6f966be2 100644 --- a/tests/test_web_observability.py +++ b/tests/test_web_observability.py @@ -6,7 +6,6 @@ import web.routes as routes import web.scan_terminal_cache as scan_terminal_cache import web.scan_terminal_service as scan_terminal_service from web.scan_terminal_cache import scan_terminal_cache_key -from src.database.db_manager import DBManager from src.database.runtime_state import TruthRecordRepository, TrainingFeatureRecordRepository @@ -38,7 +37,6 @@ def test_system_status_returns_summary_shape(): assert payload['probability']['rollout']['decision']['decision'] in {'hold', 'observe', 'promote'} assert 'training_data' in payload assert 'station_networks' in payload - assert 'prewarm' in payload assert 'truth_records' in payload['training_data'] assert 'training_features' in payload['training_data'] assert 'city_coverage' in payload['training_data'] @@ -46,55 +44,9 @@ def test_system_status_returns_summary_shape(): assert 'artifacts' in payload['training_data'] assert 'metar_entries' in payload['cache'] assert 'nmc_entries' in payload['cache'] - assert 'runtime' in payload['prewarm'] - assert 'last_summary_ok' in payload['prewarm']['runtime'] assert 'cities_count' in payload -def test_system_status_reads_shared_prewarm_runtime(monkeypatch): - monkeypatch.setenv("POLYWEATHER_DASHBOARD_PREWARM_ENABLED", "true") - monkeypatch.setenv("POLYWEATHER_PREWARM_INTERVAL_SEC", "300") - monkeypatch.setenv("POLYWEATHER_PREWARM_JITTER_SEC", "20") - - DBManager().set_payment_runtime_state( - "dashboard_prewarm", - { - "cycle_count": 3, - "success_count": 3, - "failure_count": 0, - "last_started_at": "2026-04-08T12:00:00", - "last_finished_at": "2026-04-08T12:00:05", - "last_duration_sec": 5.0, - "last_success": True, - "last_http_status": 200, - "last_error": None, - "last_requested_cities": ["shanghai", "beijing"], - "last_requested_city_count": 2, - "last_include_detail": True, - "last_include_market": True, - "last_force_refresh": False, - "last_warmed_count": 2, - "last_summary_ok": 2, - "last_detail_ok": 2, - "last_market_ok": 2, - "last_failed_count": 0, - "last_heartbeat_ts": __import__("time").time(), - "writer_mode": "standalone_process", - "writer_pid": 12345, - "writer_thread_name": "MainThread", - }, - ) - - response = client.get('/api/system/status') - - assert response.status_code == 200 - payload = response.json() - assert payload["prewarm"]["enabled"] is True - assert payload["prewarm"]["thread_alive"] is True - assert payload["prewarm"]["runtime"]["cycle_count"] >= 3 - assert payload["prewarm"]["runtime"]["last_summary_ok"] == 2 - - def test_metrics_endpoint_returns_prometheus_payload(): response = client.get('/metrics') assert response.status_code == 200 diff --git a/web/core.py b/web/core.py index 16899218..ae3519b9 100644 --- a/web/core.py +++ b/web/core.py @@ -21,7 +21,6 @@ from src.data_collection.country_networks import provider_coverage_summary from src.data_collection.city_risk_profiles import CITY_RISK_PROFILES # noqa: F401 from src.data_collection.polymarket_readonly import PolymarketReadOnlyLayer from src.auth.supabase_entitlement import SUPABASE_ENTITLEMENT, extract_bearer_token -from src.utils.prewarm_dashboard import get_prewarm_runtime_summary from src.utils.metrics import ( build_metrics_summary, counter_inc, @@ -836,6 +835,5 @@ def build_system_status_payload() -> Dict[str, Any]: "probability": _probability_summary(), "training_data": _training_data_summary(), "station_networks": provider_coverage_summary(), - "prewarm": get_prewarm_runtime_summary(), "cities_count": len(CITIES), } diff --git a/web/routers/system.py b/web/routers/system.py index 333fe51e..3391d2a7 100644 --- a/web/routers/system.py +++ b/web/routers/system.py @@ -11,7 +11,6 @@ from web.services.system_api import ( get_prometheus_metrics_response, get_system_cache_status, get_system_status_payload, - run_system_prewarm, run_system_priority_warm, ) @@ -28,23 +27,6 @@ async def system_status(): return await get_system_status_payload() -@router.post("/api/system/prewarm") -async def system_prewarm( - request: Request, - cities: Optional[str] = None, - force_refresh: bool = False, - include_detail: bool = False, - include_market: bool = False, -): - return run_system_prewarm( - request, - cities=cities, - force_refresh=force_refresh, - include_detail=include_detail, - include_market=include_market, - ) - - @router.get("/api/system/cache-status") async def system_cache_status(request: Request, cities: Optional[str] = None): return get_system_cache_status(request, cities=cities) diff --git a/web/services/city_runtime.py b/web/services/city_runtime.py index 8258d2f5..8b1f001f 100644 --- a/web/services/city_runtime.py +++ b/web/services/city_runtime.py @@ -80,7 +80,7 @@ TRACKABLE_ANALYTICS_EVENTS = { "checkout_succeeded", } -DEFAULT_PREWARM_CITIES = [ +DEFAULT_STATUS_CITIES = [ "ankara", "istanbul", "shanghai", @@ -658,7 +658,7 @@ def _normalize_city_or_404(name: str) -> str: def _normalize_city_list(raw: Optional[str]) -> list[str]: if not raw: - return list(DEFAULT_PREWARM_CITIES) + return list(DEFAULT_STATUS_CITIES) out: list[str] = [] for part in str(raw).split(","): city = str(part or "").strip().lower().replace("-", " ") diff --git a/web/services/dashboard_init_api.py b/web/services/dashboard_init_api.py index 89a73f78..5842d53d 100644 --- a/web/services/dashboard_init_api.py +++ b/web/services/dashboard_init_api.py @@ -23,7 +23,7 @@ def _resolve_default_city(request: Request) -> Optional[str]: cities_map = getattr(legacy_routes, "TIMEZONE_DEFAULT_CITIES", {}) if tz in cities_map: return cities_map[tz] - return next(iter(getattr(legacy_routes, "DEFAULT_PREWARM_CITIES", [])), None) + return next(iter(getattr(legacy_routes, "DEFAULT_STATUS_CITIES", [])), None) async def build_dashboard_init_payload(request: Request) -> Dict[str, Any]: diff --git a/web/services/system_api.py b/web/services/system_api.py index bc34fc66..61a3bee2 100644 --- a/web/services/system_api.py +++ b/web/services/system_api.py @@ -26,81 +26,11 @@ async def get_system_status_payload() -> Dict[str, Any]: return await run_in_threadpool(build_system_status_payload) -def run_system_prewarm( - request: Request, - *, - cities: Optional[str] = None, - force_refresh: bool = False, - include_detail: bool = False, - include_market: bool = False, -) -> Dict[str, Any]: - legacy_routes._assert_entitlement(request) - selected = legacy_routes._normalize_city_list(cities) - if not selected: - raise HTTPException(status_code=400, detail="No valid cities to prewarm") - - started = time.perf_counter() - warmed: list[dict[str, object]] = [] - failed: list[dict[str, object]] = [] - summary_ok = 0 - detail_ok = 0 - market_ok = 0 - - for city in selected: - city_started = time.perf_counter() - try: - legacy_routes._refresh_city_summary_cache(city, force_refresh=force_refresh) - entry: dict[str, object] = { - "city": city, - "summary": True, - "duration_ms": round((time.perf_counter() - city_started) * 1000.0, 1), - } - summary_ok += 1 - if include_detail: - legacy_routes._refresh_city_panel_cache(city, force_refresh=force_refresh) - entry["detail"] = True - detail_ok += 1 - if include_market: - entry["market"] = False - warmed.append(entry) - except Exception as exc: - failed.append( - { - "city": city, - "error": str(exc), - "duration_ms": round((time.perf_counter() - city_started) * 1000.0, 1), - } - ) - - total_ms = round((time.perf_counter() - started) * 1000.0, 1) - logger.info( - "system prewarm finished count={} failed={} force_refresh={} include_detail={} include_market={} duration_ms={}", - len(warmed), - len(failed), - force_refresh, - include_detail, - include_market, - total_ms, - ) - return { - "ok": len(failed) == 0, - "cities": selected, - "warmed": warmed, - "failed": failed, - "summary_ok": summary_ok, - "panel_ok": detail_ok, - "detail_ok": detail_ok, - "market_ok": market_ok, - "failed_count": len(failed), - "duration_ms": total_ms, - } - - def get_system_cache_status(request: Request, cities: Optional[str] = None) -> Dict[str, Any]: legacy_routes._assert_entitlement(request) selected = legacy_routes._normalize_city_list(cities) if not selected: - selected = list(legacy_routes.DEFAULT_PREWARM_CITIES) + selected = legacy_routes._normalize_city_list(None) kinds = { "summary": legacy_routes.CITY_SUMMARY_CACHE_TTL_SEC, "panel": legacy_routes.CITY_PANEL_CACHE_TTL_SEC,