Stop collector from triggering full weather refreshes
This commit is contained in:
+11
-107
@@ -7,7 +7,7 @@ import asyncio
|
||||
import threading
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
@@ -26,12 +26,6 @@ _RECENT_DEB_CACHE_TTL_SEC = max(
|
||||
60,
|
||||
int(os.getenv("POLYWEATHER_CITIES_DEB_RECENT_CACHE_TTL_SEC", "300") or "300"),
|
||||
)
|
||||
_CITY_FULL_REFRESH_INFLIGHT: Dict[str, "asyncio.Task[Dict[str, Any]]"] = {}
|
||||
_CITY_FULL_STALE_REFRESH_TASKS: Dict[str, "asyncio.Task[Dict[str, Any]]"] = {}
|
||||
_CITY_FULL_REFRESH_LOCK = asyncio.Lock()
|
||||
_CITY_FORCE_REFRESH_INFLIGHT: Dict[str, "asyncio.Task[Dict[str, Any]]"] = {}
|
||||
_CITY_FORCE_REFRESH_LOCK = asyncio.Lock()
|
||||
_CITY_STALE_REFRESH_TASKS: Dict[str, "asyncio.Task[Dict[str, Any]]"] = {}
|
||||
CityDetailPayloadCacheKey = Tuple[str, str, str, str, str, int]
|
||||
CityChartDetailPayloadCacheKey = Tuple[str, str, str, int]
|
||||
CityDetailBatchResponseCacheKey = Tuple[Tuple[str, ...], bool, str, str, str, str]
|
||||
@@ -71,14 +65,6 @@ def _city_detail_batch_response_cache_ttl() -> float:
|
||||
return max(0.0, min(30.0, value))
|
||||
|
||||
|
||||
def _city_force_refresh_timeout_sec() -> float:
|
||||
try:
|
||||
value = float(os.getenv("POLYWEATHER_CITY_FORCE_REFRESH_TIMEOUT_SEC", "8") or "8")
|
||||
except ValueError:
|
||||
value = 8.0
|
||||
return max(0.01, min(30.0, value))
|
||||
|
||||
|
||||
def _city_chart_optional_overlay_timeout_sec() -> float:
|
||||
try:
|
||||
timeout_ms = int(
|
||||
@@ -193,7 +179,6 @@ def _enqueue_collector_refresh_request(
|
||||
def _request_city_cache_refresh(
|
||||
city: str,
|
||||
kind: str,
|
||||
refresh_fn: Callable[[str, bool], Dict[str, Any]],
|
||||
) -> None:
|
||||
_enqueue_collector_refresh_request(city, kind)
|
||||
|
||||
@@ -252,34 +237,9 @@ def _queue_and_build_initializing_city_payload(city: str, *, kind: str) -> Dict[
|
||||
return _build_initializing_city_payload(city, detail_depth=kind)
|
||||
|
||||
|
||||
async def _get_or_start_city_force_refresh_task(
|
||||
key: str,
|
||||
refresh_factory: Callable[[], Awaitable[Dict[str, Any]]],
|
||||
) -> Tuple["asyncio.Task[Dict[str, Any]]", bool]:
|
||||
async with _CITY_FORCE_REFRESH_LOCK:
|
||||
task = _CITY_FORCE_REFRESH_INFLIGHT.get(key)
|
||||
started = False
|
||||
if task is None or task.done():
|
||||
task = asyncio.create_task(refresh_factory())
|
||||
_CITY_FORCE_REFRESH_INFLIGHT[key] = task
|
||||
started = True
|
||||
|
||||
def _cleanup(done: "asyncio.Task[Dict[str, Any]]") -> None:
|
||||
if _CITY_FORCE_REFRESH_INFLIGHT.get(key) is done:
|
||||
_CITY_FORCE_REFRESH_INFLIGHT.pop(key, None)
|
||||
try:
|
||||
done.result()
|
||||
except Exception as exc: # pragma: no cover - defensive background guard
|
||||
logger.warning("city force refresh failed key={}: {}", key, exc)
|
||||
|
||||
task.add_done_callback(_cleanup)
|
||||
return task, started
|
||||
|
||||
|
||||
async def _refresh_city_payload_with_stale_timeout(
|
||||
city: str,
|
||||
kind: str,
|
||||
refresh_factory: Callable[[], Awaitable[Dict[str, Any]]],
|
||||
) -> Dict[str, Any]:
|
||||
cached_before_refresh = await _get_cached_city_payload(city, kind)
|
||||
if not cached_before_refresh:
|
||||
@@ -306,19 +266,16 @@ async def _refresh_city_payload_with_stale_timeout(
|
||||
async def _refresh_city_cache_with_stale_timeout(
|
||||
city: str,
|
||||
kind: str,
|
||||
refresh_fn: Callable[[str, bool], Dict[str, Any]],
|
||||
) -> Dict[str, Any]:
|
||||
return await _refresh_city_payload_with_stale_timeout(
|
||||
city,
|
||||
kind,
|
||||
lambda: run_in_threadpool(refresh_fn, city, True),
|
||||
)
|
||||
|
||||
|
||||
def _start_city_cache_stale_refresh(
|
||||
city: str,
|
||||
kind: str,
|
||||
refresh_fn: Callable[[str, bool], Dict[str, Any]],
|
||||
) -> None:
|
||||
normalized = str(city or "").strip().lower()
|
||||
cache_kind = str(kind or "").strip().lower()
|
||||
@@ -394,54 +351,6 @@ def _overlay_cached_runway_history_from_db(city: str, payload: Dict[str, Any]) -
|
||||
return next_payload
|
||||
|
||||
|
||||
async def _refresh_city_full_cache_singleflight(city: str, force_refresh: bool) -> Dict[str, Any]:
|
||||
key = f"{city}:{bool(force_refresh)}"
|
||||
async with _CITY_FULL_REFRESH_LOCK:
|
||||
task = _CITY_FULL_REFRESH_INFLIGHT.get(key)
|
||||
if task is None:
|
||||
async def _run_refresh() -> Dict[str, Any]:
|
||||
try:
|
||||
return await run_in_threadpool(
|
||||
legacy_routes._refresh_city_full_cache,
|
||||
city,
|
||||
force_refresh,
|
||||
)
|
||||
finally:
|
||||
await _invalidate_city_detail_payload_cache(city)
|
||||
|
||||
task = asyncio.create_task(_run_refresh())
|
||||
_CITY_FULL_REFRESH_INFLIGHT[key] = task
|
||||
try:
|
||||
return await task
|
||||
finally:
|
||||
if task.done():
|
||||
async with _CITY_FULL_REFRESH_LOCK:
|
||||
if _CITY_FULL_REFRESH_INFLIGHT.get(key) is task:
|
||||
_CITY_FULL_REFRESH_INFLIGHT.pop(key, None)
|
||||
|
||||
|
||||
async def _invalidate_city_detail_payload_cache(city: str) -> None:
|
||||
normalized = str(city or "").strip().lower()
|
||||
if not normalized:
|
||||
return
|
||||
async with _CITY_DETAIL_PAYLOAD_LOCK:
|
||||
_CITY_DETAIL_PAYLOAD_EPOCH[normalized] = _CITY_DETAIL_PAYLOAD_EPOCH.get(normalized, 0) + 1
|
||||
old_keys = [key for key in _CITY_DETAIL_PAYLOAD_CACHE if key[0] == normalized]
|
||||
for key in old_keys:
|
||||
_CITY_DETAIL_PAYLOAD_CACHE.pop(key, None)
|
||||
_CITY_DETAIL_PAYLOAD_CACHE_TS.pop(key, None)
|
||||
async with _CITY_CHART_DETAIL_PAYLOAD_LOCK:
|
||||
old_chart_keys = [key for key in _CITY_CHART_DETAIL_PAYLOAD_CACHE if key[0] == normalized]
|
||||
for key in old_chart_keys:
|
||||
_CITY_CHART_DETAIL_PAYLOAD_CACHE.pop(key, None)
|
||||
_CITY_CHART_DETAIL_PAYLOAD_CACHE_TS.pop(key, None)
|
||||
|
||||
|
||||
async def _refresh_city_full_data(city: str, force_refresh: bool) -> Dict[str, Any]:
|
||||
await _invalidate_city_detail_payload_cache(city)
|
||||
return await _refresh_city_full_cache_singleflight(city, force_refresh)
|
||||
|
||||
|
||||
def _start_city_full_stale_refresh(city: str) -> None:
|
||||
normalized = str(city or "").strip().lower()
|
||||
if not normalized:
|
||||
@@ -454,7 +363,6 @@ async def _get_city_full_data(city: str, *, force_refresh: bool) -> Dict[str, An
|
||||
return await _refresh_city_payload_with_stale_timeout(
|
||||
city,
|
||||
"full",
|
||||
lambda: _refresh_city_full_data(city, True),
|
||||
)
|
||||
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "full", city)
|
||||
if cached_entry:
|
||||
@@ -792,20 +700,19 @@ async def get_city_detail_payload(
|
||||
return await _refresh_city_cache_with_stale_timeout(
|
||||
city,
|
||||
"panel",
|
||||
legacy_routes._refresh_city_panel_cache,
|
||||
)
|
||||
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "panel", city)
|
||||
if cached_entry:
|
||||
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_PANEL_CACHE_TTL_SEC):
|
||||
payload = cached_entry.get("payload") or {}
|
||||
if payload:
|
||||
_start_city_cache_stale_refresh(city, "panel", legacy_routes._refresh_city_panel_cache)
|
||||
_start_city_cache_stale_refresh(city, "panel")
|
||||
return await _overlay_cached_wunderground(city, payload)
|
||||
return _queue_and_build_initializing_city_payload(city, kind="panel")
|
||||
return await _overlay_cached_wunderground(city, cached_entry.get("payload") or {})
|
||||
canonical_payload = await _get_canonical_city_payload(city, detail_depth="panel")
|
||||
if canonical_payload:
|
||||
_request_city_cache_refresh(city, "panel", legacy_routes._refresh_city_panel_cache)
|
||||
_request_city_cache_refresh(city, "panel")
|
||||
return canonical_payload
|
||||
return _queue_and_build_initializing_city_payload(city, kind="panel")
|
||||
if detail_mode == "nearby":
|
||||
@@ -813,24 +720,23 @@ async def get_city_detail_payload(
|
||||
return await _refresh_city_cache_with_stale_timeout(
|
||||
city,
|
||||
"nearby",
|
||||
legacy_routes._refresh_city_nearby_cache,
|
||||
)
|
||||
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "nearby", city)
|
||||
if cached_entry:
|
||||
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_NEARBY_CACHE_TTL_SEC):
|
||||
payload = cached_entry.get("payload") or {}
|
||||
if payload:
|
||||
_start_city_cache_stale_refresh(city, "nearby", legacy_routes._refresh_city_nearby_cache)
|
||||
_start_city_cache_stale_refresh(city, "nearby")
|
||||
return await _overlay_cached_wunderground(city, payload)
|
||||
canonical_payload = await _get_canonical_city_payload(city, detail_depth="nearby")
|
||||
if canonical_payload:
|
||||
_request_city_cache_refresh(city, "nearby", legacy_routes._refresh_city_nearby_cache)
|
||||
_request_city_cache_refresh(city, "nearby")
|
||||
return canonical_payload
|
||||
return _queue_and_build_initializing_city_payload(city, kind="nearby")
|
||||
return await _overlay_cached_wunderground(city, cached_entry.get("payload") or {})
|
||||
canonical_payload = await _get_canonical_city_payload(city, detail_depth="nearby")
|
||||
if canonical_payload:
|
||||
_request_city_cache_refresh(city, "nearby", legacy_routes._refresh_city_nearby_cache)
|
||||
_request_city_cache_refresh(city, "nearby")
|
||||
return canonical_payload
|
||||
return _queue_and_build_initializing_city_payload(city, kind="nearby")
|
||||
if detail_mode == "market":
|
||||
@@ -838,24 +744,23 @@ async def get_city_detail_payload(
|
||||
return await _refresh_city_cache_with_stale_timeout(
|
||||
city,
|
||||
"market",
|
||||
legacy_routes._refresh_city_market_cache,
|
||||
)
|
||||
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "market", city)
|
||||
if cached_entry:
|
||||
if not legacy_routes._market_analysis_cache_is_fresh(cached_entry):
|
||||
payload = cached_entry.get("payload") or {}
|
||||
if payload:
|
||||
_start_city_cache_stale_refresh(city, "market", legacy_routes._refresh_city_market_cache)
|
||||
_start_city_cache_stale_refresh(city, "market")
|
||||
return await _overlay_cached_wunderground(city, payload)
|
||||
canonical_payload = await _get_canonical_city_payload(city, detail_depth="market")
|
||||
if canonical_payload:
|
||||
_request_city_cache_refresh(city, "market", legacy_routes._refresh_city_market_cache)
|
||||
_request_city_cache_refresh(city, "market")
|
||||
return canonical_payload
|
||||
return _queue_and_build_initializing_city_payload(city, kind="market")
|
||||
return await _overlay_cached_wunderground(city, cached_entry.get("payload") or {})
|
||||
canonical_payload = await _get_canonical_city_payload(city, detail_depth="market")
|
||||
if canonical_payload:
|
||||
_request_city_cache_refresh(city, "market", legacy_routes._refresh_city_market_cache)
|
||||
_request_city_cache_refresh(city, "market")
|
||||
return canonical_payload
|
||||
return _queue_and_build_initializing_city_payload(city, kind="market")
|
||||
return await run_in_threadpool(legacy_routes._analyze, city, force_refresh, False, detail_mode)
|
||||
@@ -872,20 +777,19 @@ async def get_city_summary_payload(
|
||||
return await _refresh_city_cache_with_stale_timeout(
|
||||
city,
|
||||
"summary",
|
||||
legacy_routes._refresh_city_summary_cache,
|
||||
)
|
||||
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "summary", city)
|
||||
if cached_entry:
|
||||
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_SUMMARY_CACHE_TTL_SEC):
|
||||
payload = cached_entry.get("payload") or {}
|
||||
if payload:
|
||||
_start_city_cache_stale_refresh(city, "summary", legacy_routes._refresh_city_summary_cache)
|
||||
_start_city_cache_stale_refresh(city, "summary")
|
||||
return await _overlay_cached_wunderground(city, payload)
|
||||
return _queue_and_build_initializing_city_payload(city, kind="summary")
|
||||
return await _overlay_cached_wunderground(city, cached_entry.get("payload") or {})
|
||||
canonical_payload = await _get_canonical_city_payload(city, detail_depth="summary")
|
||||
if canonical_payload:
|
||||
_request_city_cache_refresh(city, "summary", legacy_routes._refresh_city_summary_cache)
|
||||
_request_city_cache_refresh(city, "summary")
|
||||
return canonical_payload
|
||||
return _queue_and_build_initializing_city_payload(city, kind="summary")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user