fix(site-finder): optimize zone-regulation resolve in /analyze (#1850) #1901

Merged
bot-backend merged 1 commit from fix/analyze-zone-resolve-perf into main 2026-06-25 09:12:42 +00:00
Collaborator

Closes #1850. Hot-path perf для /analyze — устраняет лишние live WFS round-trip'ы при резолве ПЗЗ-регламента.

Проблема

get_or_fetch_zone_regulation зовёт client.zone_index_at(lon,lat) (live WFS) безусловно на каждый analyze, даже при тёплом кэше. Хуже — при обоих флагах (enable_zoning_regulation_in_analyze + enable_ird_analyze, оба default on) резолвер зовётся дважды за запрос → 2× WFS. Плюс таймаут дрейфовал (parcels 3s vs ird 4s).

Фикс

  1. Мемоизация coord→zone_index (маппинг статичен — границы зон не двигаются): bounded LRU по округлённым до 5 знаков (~1м) координатам, с негативным кэшированием (участки вне ЕКБ не пере-дёргают WFS). Кэшируется ТОЛЬКО индекс зоны — содержимое регламента читается свежим из zone_regulation_cache (корректность обновления кэша сохранена).
  2. Резолв один раз: build_ird_analyze_block получил resolved_zone_regulation kwarg — parcels.py прокидывает уже резолвнутый регламент, IRD-блок не резолвит повторно (все 4 комбинации флагов прослежены).
  3. Единый таймаут settings.geoportal_timeout_s=4 вместо двух расходящихся констант.

Thread-safety (deep-review HIGH — исправлено)

/analyze — sync def → исполняется в Starlette threadpool, т.е. несколько потоков мутируют module-global memo одновременно. Свой OrderedDict-LRU (в отличие от functools.lru_cache) не атомарен → check-then-act против чужого popitem даёт KeyError. Фикс: мутации под threading.Lock, WFS-вызов — вне лока (медленный геопортал не сериализует тёплые потоки). +concurrency smoke-тест (8 потоков × 200 итераций, tiny maxsize → форсим эвикцию).

Не тронуто (отдельная задача)

Mid-request db.commit() в резолвере на cache-miss (коммитит внешнюю analyze-tx) — задокументирован как SMELL, не фиксил (риск: правильный фикс = отдельная write-session). Мемоизация делает cache-miss реже, так что эффект снижен.

Тесты

test_zone_regulation_memo.py (7: memo-hit, негативное кэширование, округление с обеих сторон, LRU-эвикция, reset, concurrency) + test_ird_analyze.py (Fix-2 прокидывание). 33 passed, ruff+mypy clean. Deep-review (после lock-фикса).

Backend-only, без schema/API изменений → codegen не нужен.

Closes #1850. Hot-path perf для `/analyze` — устраняет лишние live WFS round-trip'ы при резолве ПЗЗ-регламента. ## Проблема `get_or_fetch_zone_regulation` зовёт `client.zone_index_at(lon,lat)` (live WFS) **безусловно** на каждый analyze, даже при тёплом кэше. Хуже — при обоих флагах (`enable_zoning_regulation_in_analyze` + `enable_ird_analyze`, оба default on) резолвер зовётся **дважды** за запрос → 2× WFS. Плюс таймаут дрейфовал (parcels 3s vs ird 4s). ## Фикс 1. **Мемоизация** coord→zone_index (маппинг статичен — границы зон не двигаются): bounded LRU по округлённым до 5 знаков (~1м) координатам, с негативным кэшированием (участки вне ЕКБ не пере-дёргают WFS). Кэшируется ТОЛЬКО индекс зоны — **содержимое регламента читается свежим** из `zone_regulation_cache` (корректность обновления кэша сохранена). 2. **Резолв один раз**: `build_ird_analyze_block` получил `resolved_zone_regulation` kwarg — parcels.py прокидывает уже резолвнутый регламент, IRD-блок не резолвит повторно (все 4 комбинации флагов прослежены). 3. **Единый таймаут** `settings.geoportal_timeout_s=4` вместо двух расходящихся констант. ## Thread-safety (deep-review HIGH — исправлено) `/analyze` — sync def → исполняется в Starlette threadpool, т.е. несколько потоков мутируют module-global memo одновременно. Свой OrderedDict-LRU (в отличие от `functools.lru_cache`) не атомарен → check-then-act против чужого `popitem` даёт KeyError. **Фикс:** мутации под `threading.Lock`, WFS-вызов — вне лока (медленный геопортал не сериализует тёплые потоки). +concurrency smoke-тест (8 потоков × 200 итераций, tiny maxsize → форсим эвикцию). ## Не тронуто (отдельная задача) Mid-request `db.commit()` в резолвере на cache-miss (коммитит внешнюю analyze-tx) — задокументирован как SMELL, не фиксил (риск: правильный фикс = отдельная write-session). Мемоизация делает cache-miss реже, так что эффект снижен. ## Тесты `test_zone_regulation_memo.py` (7: memo-hit, негативное кэширование, округление с обеих сторон, LRU-эвикция, reset, concurrency) + `test_ird_analyze.py` (Fix-2 прокидывание). 33 passed, ruff+mypy clean. Deep-review ✅ (после lock-фикса). Backend-only, без schema/API изменений → codegen не нужен.
bot-backend added 1 commit 2026-06-25 09:00:47 +00:00
fix(site-finder): memoize zone_index WFS lookup + dedupe resolve in /analyze (#1850)
All checks were successful
CI / backend-tests (pull_request) Successful in 11m8s
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m46s
18942898d1
- memoize coord→zone_index (bounded thread-safe LRU, ±1m key, negative caching) so
  repeat /analyze skips the live WFS round-trip; regulation content still read fresh
  from zone_regulation_cache (cache-update correctness preserved). Lock guards the
  module-global memo (sync /analyze runs in threadpool); WFS call stays outside lock.
- resolve once, thread into build_ird_analyze_block (resolved_zone_regulation kwarg)
  → no redundant 2nd resolve when both enable_zoning_regulation_in_analyze and
  enable_ird_analyze are on.
- unify divergent timeouts (parcels 3s / ird 4s) into settings.geoportal_timeout_s=4.
- document (not change) the mid-request db.commit() smell on cache-miss (separate task).

Closes #1850
bot-backend merged commit d2773e0c91 into main 2026-06-25 09:12:42 +00:00
bot-backend deleted branch fix/analyze-zone-resolve-perf 2026-06-25 09:12:42 +00:00
Sign in to join this conversation.
No reviewers
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#1901
No description provided.