Files
PolyWeather/src/data_collection/open_meteo_cache.py
T
2026-04-16 00:52:55 +08:00

185 lines
8.5 KiB
Python

from __future__ import annotations
import json
import os
import time
from loguru import logger
from src.database.runtime_state import (
OpenMeteoCacheRepository,
STATE_STORAGE_SQLITE,
get_state_storage_mode,
)
_open_meteo_cache_repo = OpenMeteoCacheRepository()
class OpenMeteoCacheMixin:
def _load_open_meteo_disk_cache(self) -> None:
"""启动时从磁盘加载 Open-Meteo 三类缓存,避免重启后冷启动打爆 API"""
mode = get_state_storage_mode()
if mode == STATE_STORAGE_SQLITE:
try:
saved = _open_meteo_cache_repo.load_payload(self._disk_cache_max_age_sec)
loaded = self._merge_open_meteo_payload(saved)
self._disk_cache_last_mtime = _open_meteo_cache_repo.latest_updated_at()
if loaded:
logger.info(f"✅ 从 SQLite 加载 Open-Meteo 缓存 {loaded} 条")
return
except Exception as exc:
logger.error(f"SQLite Open-Meteo 缓存加载失败,fallback file: {exc}")
try:
path = self._disk_cache_path
if not os.path.exists(path):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(
{
"forecast": {},
"ensemble": {},
"multi_model": {},
"saved_at": time.time(),
},
f,
)
self._disk_cache_last_mtime = os.path.getmtime(path)
return
current_mtime = os.path.getmtime(path)
if current_mtime <= self._disk_cache_last_mtime:
return
with open(path, "r", encoding="utf-8") as f:
saved = json.load(f)
now = time.time()
max_age = max(600, self._disk_cache_max_age_sec)
loaded = 0
with self._open_meteo_cache_lock:
for key, entry in saved.get("forecast", {}).items():
if now - float(entry.get("t", 0)) < max_age:
old = self._open_meteo_cache.get(key)
if old is None or float(entry.get("t", 0)) >= float(old.get("t", 0)):
self._open_meteo_cache[key] = entry
loaded += 1
with self._ensemble_cache_lock:
for key, entry in saved.get("ensemble", {}).items():
if now - float(entry.get("t", 0)) < max_age:
old = self._ensemble_cache.get(key)
if old is None or float(entry.get("t", 0)) >= float(old.get("t", 0)):
self._ensemble_cache[key] = entry
loaded += 1
with self._multi_model_cache_lock:
for key, entry in saved.get("multi_model", {}).items():
if now - float(entry.get("t", 0)) < max_age:
old = self._multi_model_cache.get(key)
if old is None or float(entry.get("t", 0)) >= float(old.get("t", 0)):
self._multi_model_cache[key] = entry
loaded += 1
self._disk_cache_last_mtime = current_mtime
if loaded:
logger.info(f"✅ 从磁盘加载 Open-Meteo 缓存 {loaded} 条 ({self._disk_cache_path})")
if mode == STATE_STORAGE_SQLITE:
try:
_open_meteo_cache_repo.replace_payload(saved, self._disk_cache_max_age_sec)
except Exception as exc:
logger.warning(f"SQLite Open-Meteo 缓存同步失败: {exc}")
except Exception as exc:
logger.warning(f"磁盘缓存加载失败(首次启动不影响运行): {exc}")
def _maybe_reload_open_meteo_disk_cache(self) -> None:
"""跨进程共享缓存:当缓存文件有更新时增量重载到当前进程内存"""
mode = get_state_storage_mode()
if mode == STATE_STORAGE_SQLITE:
try:
latest_updated_at = _open_meteo_cache_repo.latest_updated_at()
if latest_updated_at <= self._disk_cache_last_mtime:
return
self._load_open_meteo_disk_cache()
except Exception:
pass
return
try:
path = self._disk_cache_path
if not os.path.exists(path):
return
current_mtime = os.path.getmtime(path)
if current_mtime <= self._disk_cache_last_mtime:
return
self._load_open_meteo_disk_cache()
except Exception:
pass
def _flush_open_meteo_disk_cache(self) -> None:
"""将三类 Open-Meteo 内存缓存持久化到磁盘"""
mode = get_state_storage_mode()
try:
with self._open_meteo_cache_lock:
forecast_snapshot = dict(self._open_meteo_cache)
with self._ensemble_cache_lock:
ensemble_snapshot = dict(self._ensemble_cache)
with self._multi_model_cache_lock:
multi_model_snapshot = dict(self._multi_model_cache)
payload = {
"forecast": forecast_snapshot,
"ensemble": ensemble_snapshot,
"multi_model": multi_model_snapshot,
"saved_at": time.time(),
}
if mode == STATE_STORAGE_SQLITE:
_open_meteo_cache_repo.replace_payload(payload, self._disk_cache_max_age_sec)
self._disk_cache_last_mtime = _open_meteo_cache_repo.latest_updated_at()
if mode != STATE_STORAGE_SQLITE:
os.makedirs(os.path.dirname(self._disk_cache_path), exist_ok=True)
with self._disk_cache_lock:
tmp_path = self._disk_cache_path + ".tmp"
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(payload, f)
os.replace(tmp_path, self._disk_cache_path)
self._disk_cache_last_mtime = max(
self._disk_cache_last_mtime,
os.path.getmtime(self._disk_cache_path),
)
except Exception as exc:
logger.warning(f"磁盘缓存写入失败: {exc}")
def _merge_open_meteo_payload(self, saved: dict) -> int:
now = time.time()
max_age = max(600, self._disk_cache_max_age_sec)
loaded = 0
with self._open_meteo_cache_lock:
for key, entry in (saved.get("forecast", {}) or {}).items():
if now - float(entry.get("t", 0)) < max_age:
old = self._open_meteo_cache.get(key)
if old is None or float(entry.get("t", 0)) >= float(old.get("t", 0)):
self._open_meteo_cache[key] = entry
loaded += 1
with self._ensemble_cache_lock:
for key, entry in (saved.get("ensemble", {}) or {}).items():
if now - float(entry.get("t", 0)) < max_age:
old = self._ensemble_cache.get(key)
if old is None or float(entry.get("t", 0)) >= float(old.get("t", 0)):
self._ensemble_cache[key] = entry
loaded += 1
with self._multi_model_cache_lock:
for key, entry in (saved.get("multi_model", {}) or {}).items():
if now - float(entry.get("t", 0)) < max_age:
old = self._multi_model_cache.get(key)
if old is None or float(entry.get("t", 0)) >= float(old.get("t", 0)):
self._multi_model_cache[key] = entry
loaded += 1
return loaded
def _wait_open_meteo_slot(self, endpoint: str) -> None:
"""Simple per-process rate gate for Open-Meteo endpoints."""
min_interval = self._open_meteo_min_interval_sec
if min_interval <= 0:
return
with self._open_meteo_call_lock:
now_ts = time.time()
wait_for = min_interval - (now_ts - self._open_meteo_last_call_ts)
if wait_for > 0:
logger.debug(
f"Open-Meteo {endpoint} 限流保护:sleep {wait_for:.2f}s (min_interval={min_interval:.2f}s)"
)
time.sleep(wait_for)
now_ts = time.time()
self._open_meteo_last_call_ts = now_ts