完全移除预热(prewarm)功能
- 删除 src/utils/prewarm_dashboard.py - 删除 scripts/prewarm_dashboard_cache.py、prewarm_dashboard_worker.py - docker-compose.yml 移除 polyweather_prewarm 服务 - runtime_coordinator.py 移除预热循环启动逻辑 - web/core.py 移除 prewarm status 上报 - web/routers/system.py 移除 /api/system/prewarm 端点 - web/services/system_api.py 移除 run_system_prewarm - city_runtime.py DEFAULT_PREWARM_CITIES 改名为 DEFAULT_STATUS_CITIES - 清理 env.example、测试、VPS .env 中的预热配置
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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("-", " ")
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user