完全移除预热(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_AI_MAX_TOKENS=3200
|
||||||
POLYWEATHER_SCAN_CITY_AI_MAX_TOKENS=900
|
POLYWEATHER_SCAN_CITY_AI_MAX_TOKENS=900
|
||||||
POLYWEATHER_SCAN_AI_PROXY_TIMEOUT_MS=55000
|
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_SUMMARY_CACHE_TTL_SEC=1800
|
||||||
POLYWEATHER_CITY_PANEL_CACHE_TTL_SEC=1800
|
POLYWEATHER_CITY_PANEL_CACHE_TTL_SEC=1800
|
||||||
POLYWEATHER_CITY_NEARBY_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.
|
# UID/GID are mainly useful on Linux hosts to avoid root-owned output files.
|
||||||
user: "${UID:-1000}:${GID:-1000}"
|
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:
|
def start_all(self) -> RuntimeStatus:
|
||||||
loops = [
|
loops = [
|
||||||
self._start_airport_high_freq_loop(),
|
self._start_airport_high_freq_loop(),
|
||||||
self._start_dashboard_prewarm_loop(),
|
|
||||||
self._start_polygon_wallet_loop(),
|
self._start_polygon_wallet_loop(),
|
||||||
self._start_polymarket_wallet_activity_loop(),
|
self._start_polymarket_wallet_activity_loop(),
|
||||||
self._start_weekly_reward_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:
|
def _start_polygon_wallet_loop(self) -> LoopStatus:
|
||||||
enabled = _env_bool("POLYGON_WALLET_WATCH_ENABLED", False)
|
enabled = _env_bool("POLYGON_WALLET_WATCH_ENABLED", False)
|
||||||
chat_ids = get_telegram_chat_ids_from_env()
|
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_cache as scan_terminal_cache
|
||||||
import web.scan_terminal_service as scan_terminal_service
|
import web.scan_terminal_service as scan_terminal_service
|
||||||
from web.scan_terminal_cache import scan_terminal_cache_key
|
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
|
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 payload['probability']['rollout']['decision']['decision'] in {'hold', 'observe', 'promote'}
|
||||||
assert 'training_data' in payload
|
assert 'training_data' in payload
|
||||||
assert 'station_networks' in payload
|
assert 'station_networks' in payload
|
||||||
assert 'prewarm' in payload
|
|
||||||
assert 'truth_records' in payload['training_data']
|
assert 'truth_records' in payload['training_data']
|
||||||
assert 'training_features' in payload['training_data']
|
assert 'training_features' in payload['training_data']
|
||||||
assert 'city_coverage' 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 'artifacts' in payload['training_data']
|
||||||
assert 'metar_entries' in payload['cache']
|
assert 'metar_entries' in payload['cache']
|
||||||
assert 'nmc_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
|
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():
|
def test_metrics_endpoint_returns_prometheus_payload():
|
||||||
response = client.get('/metrics')
|
response = client.get('/metrics')
|
||||||
assert response.status_code == 200
|
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.city_risk_profiles import CITY_RISK_PROFILES # noqa: F401
|
||||||
from src.data_collection.polymarket_readonly import PolymarketReadOnlyLayer
|
from src.data_collection.polymarket_readonly import PolymarketReadOnlyLayer
|
||||||
from src.auth.supabase_entitlement import SUPABASE_ENTITLEMENT, extract_bearer_token
|
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 (
|
from src.utils.metrics import (
|
||||||
build_metrics_summary,
|
build_metrics_summary,
|
||||||
counter_inc,
|
counter_inc,
|
||||||
@@ -836,6 +835,5 @@ def build_system_status_payload() -> Dict[str, Any]:
|
|||||||
"probability": _probability_summary(),
|
"probability": _probability_summary(),
|
||||||
"training_data": _training_data_summary(),
|
"training_data": _training_data_summary(),
|
||||||
"station_networks": provider_coverage_summary(),
|
"station_networks": provider_coverage_summary(),
|
||||||
"prewarm": get_prewarm_runtime_summary(),
|
|
||||||
"cities_count": len(CITIES),
|
"cities_count": len(CITIES),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from web.services.system_api import (
|
|||||||
get_prometheus_metrics_response,
|
get_prometheus_metrics_response,
|
||||||
get_system_cache_status,
|
get_system_cache_status,
|
||||||
get_system_status_payload,
|
get_system_status_payload,
|
||||||
run_system_prewarm,
|
|
||||||
run_system_priority_warm,
|
run_system_priority_warm,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,23 +27,6 @@ async def system_status():
|
|||||||
return await get_system_status_payload()
|
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")
|
@router.get("/api/system/cache-status")
|
||||||
async def system_cache_status(request: Request, cities: Optional[str] = None):
|
async def system_cache_status(request: Request, cities: Optional[str] = None):
|
||||||
return get_system_cache_status(request, cities=cities)
|
return get_system_cache_status(request, cities=cities)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ TRACKABLE_ANALYTICS_EVENTS = {
|
|||||||
"checkout_succeeded",
|
"checkout_succeeded",
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_PREWARM_CITIES = [
|
DEFAULT_STATUS_CITIES = [
|
||||||
"ankara",
|
"ankara",
|
||||||
"istanbul",
|
"istanbul",
|
||||||
"shanghai",
|
"shanghai",
|
||||||
@@ -658,7 +658,7 @@ def _normalize_city_or_404(name: str) -> str:
|
|||||||
|
|
||||||
def _normalize_city_list(raw: Optional[str]) -> list[str]:
|
def _normalize_city_list(raw: Optional[str]) -> list[str]:
|
||||||
if not raw:
|
if not raw:
|
||||||
return list(DEFAULT_PREWARM_CITIES)
|
return list(DEFAULT_STATUS_CITIES)
|
||||||
out: list[str] = []
|
out: list[str] = []
|
||||||
for part in str(raw).split(","):
|
for part in str(raw).split(","):
|
||||||
city = str(part or "").strip().lower().replace("-", " ")
|
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", {})
|
cities_map = getattr(legacy_routes, "TIMEZONE_DEFAULT_CITIES", {})
|
||||||
if tz in cities_map:
|
if tz in cities_map:
|
||||||
return cities_map[tz]
|
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]:
|
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)
|
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]:
|
def get_system_cache_status(request: Request, cities: Optional[str] = None) -> Dict[str, Any]:
|
||||||
legacy_routes._assert_entitlement(request)
|
legacy_routes._assert_entitlement(request)
|
||||||
selected = legacy_routes._normalize_city_list(cities)
|
selected = legacy_routes._normalize_city_list(cities)
|
||||||
if not selected:
|
if not selected:
|
||||||
selected = list(legacy_routes.DEFAULT_PREWARM_CITIES)
|
selected = legacy_routes._normalize_city_list(None)
|
||||||
kinds = {
|
kinds = {
|
||||||
"summary": legacy_routes.CITY_SUMMARY_CACHE_TTL_SEC,
|
"summary": legacy_routes.CITY_SUMMARY_CACHE_TTL_SEC,
|
||||||
"panel": legacy_routes.CITY_PANEL_CACHE_TTL_SEC,
|
"panel": legacy_routes.CITY_PANEL_CACHE_TTL_SEC,
|
||||||
|
|||||||
Reference in New Issue
Block a user