(low) Глобальный per-cache Lock удерживается на время сетевого вызова Open-Meteo — сериализация всех analyze-запросов одного cache-типа #1370

Closed
opened 2026-06-14 18:09:50 +00:00 by bot-backend · 0 comments
Collaborator

Описание

В get_weather_cached (и зеркально get_seasonal_weather_cached:279-287, get_air_quality_cached:334-342) сетевой httpx-вызов выполняется ВНУТРИ with _FORECAST_LOCK:. Lock — per-cache-type (не per-key), поэтому single-flight работает не для одного ключа, как утверждает docstring, а сериализует ЛЮБЫЕ конкурентные запросы одного cache-типа. Под Uvicorn-threadpool при холодном кэше или истёкшем negative-TTL каждый analyze для РАЗНЫХ участков (разные округлённые координаты) встаёт в очередь за чужим вызовом, который висит до _WEATHER_TIMEOUT_S=2s / _SEASONAL_TIMEOUT_S=3s.

Корень

Network I/O выполняется под общим cache-level lock вместо защиты только check/store. Уточнение находки: lock'ов три (_FORECAST_LOCK, _CLIMATE_LOCK, _AIR_LOCK — weather_cache.py:70/73/76), по одному на тип кэша, а не «один на весь кэш» — forecast и climate/air не блокируют друг друга. Но суть дефекта реальна: каждый lock per-cache-type, НЕ per-key.

Evidence

weather_cache.py:257-267:

with _FORECAST_LOCK:
    entry = _FORECAST_CACHE.get(key)
    if entry is not None and entry[1] > now:
        return entry[0]
    # MISS или истёк → внутри lock'а делаем сетевой вызов
    value = _fetch_weather_remote(lat, lon)

Docstring (строки 29-31) декларирует «Single-flight на ОДИН ключ», но реализация блокирует все ключи одного cache-типа.

Suggested fix

Single-flight per-key: держать per-key threading.Event/Lock (dict ключ→Lock под коротким мета-локом), отпуская глобальный lock на время network I/O. Минимально: вынести _fetch_*_remote за пределы общего lock, защитив только check/store, допустив редкий дубль-fetch на один ключ (приемлемо для best-effort кэша).

Обоснование severity

low: модульный docstring (строки 21-23) явно признаёт «ЕКБ-wide один cad ≈ один ключ» — в проде конкуренция идёт за ОДИН ключ, и per-cache-type lock даёт тот же эффект что и правильный per-key lock. Дефект латентен, проявится при мульти-городском масштабировании (дрейф ключей). confidence 0.82.


week ревью 1 · multi-agent аудит 2026-06-14 · severity=low · category=async-concurrency

Затронутые файлы:

  • /Users/anton/Птица/gendesign/backend/app/services/weather_cache.py
## Описание В `get_weather_cached` (и зеркально `get_seasonal_weather_cached:279-287`, `get_air_quality_cached:334-342`) сетевой httpx-вызов выполняется ВНУТРИ `with _FORECAST_LOCK:`. Lock — per-cache-type (не per-key), поэтому single-flight работает не для одного ключа, как утверждает docstring, а сериализует ЛЮБЫЕ конкурентные запросы одного cache-типа. Под Uvicorn-threadpool при холодном кэше или истёкшем negative-TTL каждый analyze для РАЗНЫХ участков (разные округлённые координаты) встаёт в очередь за чужим вызовом, который висит до `_WEATHER_TIMEOUT_S=2s` / `_SEASONAL_TIMEOUT_S=3s`. ## Корень Network I/O выполняется под общим cache-level lock вместо защиты только check/store. Уточнение находки: lock'ов три (`_FORECAST_LOCK`, `_CLIMATE_LOCK`, `_AIR_LOCK` — weather_cache.py:70/73/76), по одному на тип кэша, а не «один на весь кэш» — forecast и climate/air не блокируют друг друга. Но суть дефекта реальна: каждый lock per-cache-type, НЕ per-key. ## Evidence `weather_cache.py:257-267`: ```python with _FORECAST_LOCK: entry = _FORECAST_CACHE.get(key) if entry is not None and entry[1] > now: return entry[0] # MISS или истёк → внутри lock'а делаем сетевой вызов value = _fetch_weather_remote(lat, lon) ``` Docstring (строки 29-31) декларирует «Single-flight на ОДИН ключ», но реализация блокирует все ключи одного cache-типа. ## Suggested fix Single-flight per-key: держать per-key `threading.Event`/`Lock` (dict ключ→Lock под коротким мета-локом), отпуская глобальный lock на время network I/O. Минимально: вынести `_fetch_*_remote` за пределы общего lock, защитив только check/store, допустив редкий дубль-fetch на один ключ (приемлемо для best-effort кэша). ## Обоснование severity low: модульный docstring (строки 21-23) явно признаёт «ЕКБ-wide один cad ≈ один ключ» — в проде конкуренция идёт за ОДИН ключ, и per-cache-type lock даёт тот же эффект что и правильный per-key lock. Дефект латентен, проявится при мульти-городском масштабировании (дрейф ключей). confidence 0.82. --- _week ревью 1 · multi-agent аудит 2026-06-14 · severity=**low** · category=`async-concurrency`_ Затронутые файлы: - `/Users/anton/Птица/gendesign/backend/app/services/weather_cache.py`
bot-backend added the
week ревью 1
label 2026-06-14 18:09:50 +00:00
bot-backend added the
scope/backend
label 2026-06-15 11:01:54 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1370
No description provided.