fix chart detail batch cache fallback
This commit is contained in:
@@ -925,6 +925,93 @@ def test_chart_scope_overlays_collector_runway_history_from_db(monkeypatch):
|
||||
assert history[-1] == {"time": "2026-06-06T05:28:00+00:00", "temp": 24.8}
|
||||
|
||||
|
||||
def test_chart_data_cache_hit_does_not_start_full_stale_refresh(monkeypatch):
|
||||
import asyncio
|
||||
|
||||
class FakeCache:
|
||||
def get_city_cache(self, kind, city):
|
||||
assert kind == "full"
|
||||
return {
|
||||
"payload": {
|
||||
"name": city,
|
||||
"display_name": city.title(),
|
||||
"hourly": {"times": ["13:00"], "temps": [25.0]},
|
||||
},
|
||||
}
|
||||
|
||||
def get_runway_obs_recent(self, icao, minutes=60):
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(city_api.legacy_routes, "_CACHE_DB", FakeCache())
|
||||
monkeypatch.setattr(
|
||||
city_api.legacy_routes,
|
||||
"_city_cache_is_fresh",
|
||||
lambda entry, ttl: False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
city_api,
|
||||
"_start_city_full_stale_refresh",
|
||||
lambda city: (_ for _ in ()).throw(AssertionError("chart scope must not start full stale refresh")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
city_api.legacy_routes,
|
||||
"_overlay_latest_wunderground_current",
|
||||
lambda city, payload: payload,
|
||||
)
|
||||
|
||||
payload = asyncio.run(city_api._get_city_chart_data("paris", force_refresh=False))
|
||||
|
||||
assert payload["hourly"]["temps"] == [25.0]
|
||||
|
||||
|
||||
def test_chart_data_returns_cached_payload_when_optional_overlay_times_out(monkeypatch):
|
||||
import asyncio
|
||||
|
||||
class FakeCache:
|
||||
def get_city_cache(self, kind, city):
|
||||
assert kind == "full"
|
||||
return {
|
||||
"payload": {
|
||||
"name": city,
|
||||
"display_name": city.title(),
|
||||
"risk": {"icao": "ZSPD"},
|
||||
"hourly": {"times": ["13:00"], "temps": [25.0]},
|
||||
"runway_plate_history": {
|
||||
"35R/17L": [{"time": "2026-06-06T05:21:00+00:00", "temp": 24.2}]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def get_runway_obs_recent(self, icao, minutes=60):
|
||||
return [
|
||||
{
|
||||
"runway": "35R/17L",
|
||||
"target_runway_max": 24.8,
|
||||
"otime_utc": "2026-06-06T05:28:00+00:00",
|
||||
}
|
||||
]
|
||||
|
||||
async def fake_run_in_threadpool(fn, *args, **kwargs):
|
||||
if fn is city_api._overlay_cached_runway_history_from_db:
|
||||
await asyncio.sleep(0.05)
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
monkeypatch.setenv("POLYWEATHER_CITY_CHART_OPTIONAL_OVERLAY_TIMEOUT_MS", "1")
|
||||
monkeypatch.setattr(city_api, "run_in_threadpool", fake_run_in_threadpool)
|
||||
monkeypatch.setattr(city_api.legacy_routes, "_CACHE_DB", FakeCache())
|
||||
monkeypatch.setattr(
|
||||
city_api.legacy_routes,
|
||||
"_overlay_latest_wunderground_current",
|
||||
lambda city, payload: payload,
|
||||
)
|
||||
|
||||
payload = asyncio.run(city_api._get_city_chart_data("shanghai", force_refresh=False))
|
||||
|
||||
assert payload["runway_plate_history"]["35R/17L"] == [
|
||||
{"time": "2026-06-06T05:21:00+00:00", "temp": 24.2}
|
||||
]
|
||||
|
||||
|
||||
def test_chart_detail_payload_uses_threadpool_and_reuses_short_cache(monkeypatch):
|
||||
import asyncio
|
||||
|
||||
|
||||
@@ -78,6 +78,49 @@ def _city_force_refresh_timeout_sec() -> float:
|
||||
return max(0.01, min(30.0, value))
|
||||
|
||||
|
||||
def _city_chart_optional_overlay_timeout_sec() -> float:
|
||||
try:
|
||||
timeout_ms = int(
|
||||
os.getenv("POLYWEATHER_CITY_CHART_OPTIONAL_OVERLAY_TIMEOUT_MS", "500")
|
||||
or "500"
|
||||
)
|
||||
except ValueError:
|
||||
timeout_ms = 500
|
||||
return max(0.001, min(3.0, timeout_ms / 1000.0))
|
||||
|
||||
|
||||
async def _run_optional_city_chart_overlay(
|
||||
*,
|
||||
city: str,
|
||||
overlay_name: str,
|
||||
payload: Dict[str, Any],
|
||||
fn: Callable[..., Dict[str, Any]],
|
||||
args: Tuple[Any, ...],
|
||||
) -> Dict[str, Any]:
|
||||
timeout_sec = _city_chart_optional_overlay_timeout_sec()
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
run_in_threadpool(fn, *args),
|
||||
timeout=timeout_sec,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(
|
||||
"city chart optional overlay timed out city={} overlay={} timeout_sec={}; returning cached payload",
|
||||
city,
|
||||
overlay_name,
|
||||
timeout_sec,
|
||||
)
|
||||
return payload
|
||||
except Exception as exc:
|
||||
logger.debug(
|
||||
"city chart optional overlay skipped city={} overlay={}: {}",
|
||||
city,
|
||||
overlay_name,
|
||||
exc,
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
async def _get_cached_city_payload(city: str, kind: str) -> Dict[str, Any]:
|
||||
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, kind, city)
|
||||
if not isinstance(cached_entry, dict):
|
||||
@@ -354,14 +397,20 @@ async def _get_city_chart_data(city: str, *, force_refresh: bool) -> Dict[str, A
|
||||
if cached_entry:
|
||||
payload = cached_entry.get("payload") or {}
|
||||
if payload:
|
||||
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_FULL_CACHE_TTL_SEC):
|
||||
_start_city_full_stale_refresh(city)
|
||||
payload = await run_in_threadpool(
|
||||
_overlay_cached_runway_history_from_db,
|
||||
city,
|
||||
payload,
|
||||
payload = await _run_optional_city_chart_overlay(
|
||||
city=city,
|
||||
overlay_name="runway_history",
|
||||
payload=payload,
|
||||
fn=_overlay_cached_runway_history_from_db,
|
||||
args=(city, payload),
|
||||
)
|
||||
return await _run_optional_city_chart_overlay(
|
||||
city=city,
|
||||
overlay_name="wunderground_current",
|
||||
payload=payload,
|
||||
fn=legacy_routes._overlay_latest_wunderground_current,
|
||||
args=(city, payload),
|
||||
)
|
||||
return await _overlay_cached_wunderground(city, payload)
|
||||
|
||||
return {
|
||||
"name": city,
|
||||
|
||||
Reference in New Issue
Block a user