perf(sf): concurrent geoportal-вызовы в ИРД-блоке analyze (#1102) #1123

Merged
bot-reviewer merged 1 commit from feat/ird-analyze-concurrent-geoportal into main 2026-06-07 10:47:26 +00:00
Owner

Что

Чинит latency-регрессию /parcels/{cad}/analyze после включения enable_ird_analyze: 2 geoportal-вызова (krt WFS + zone_regulation) шли последовательно → ~9.4s на холодном cad (verified prod). Теперь — конкурентно.

Как

  • build_ird_analyze_block: _krt_at уходит в ThreadPoolExecutor (чистая сеть, без db), _zone_regulation_at(db,…) выполняется в основном потоке параллельно → итог ≈ max(call) ≈ ~4s вместо суммы ~8s.
  • Session-safety: SQLAlchemy Session строго в основном потоке (krt-поток db не трогает) — инвариант соблюдён, без отдельного SessionLocal.
  • Graceful-семантика и форма ответа не меняются.

Тест

test_ird_analyze.py7 passed: оба источника попадают в block; исключение в krt → krt=[], zone_regulation всё равно заполнен; Session не шарится.

Закрывает latency-хвост #1102. Часть #1067.

## Что Чинит latency-регрессию `/parcels/{cad}/analyze` после включения `enable_ird_analyze`: 2 geoportal-вызова (krt WFS + zone_regulation) шли **последовательно** → ~9.4s на холодном cad (verified prod). Теперь — **конкурентно**. ## Как - `build_ird_analyze_block`: `_krt_at` уходит в `ThreadPoolExecutor` (чистая сеть, без `db`), `_zone_regulation_at(db,…)` выполняется в основном потоке параллельно → итог ≈ `max(call)` ≈ ~4s вместо суммы ~8s. - **Session-safety**: SQLAlchemy `Session` строго в основном потоке (krt-поток `db` не трогает) — инвариант соблюдён, без отдельного SessionLocal. - Graceful-семантика и форма ответа не меняются. ## Тест `test_ird_analyze.py` — **7 passed**: оба источника попадают в block; исключение в krt → `krt=[]`, zone_regulation всё равно заполнен; Session не шарится. Закрывает latency-хвост #1102. Часть #1067.
lekss361 added 1 commit 2026-06-07 10:44:14 +00:00
perf(sf): concurrent geoportal-вызовы в ИРД-блоке analyze (#1102)
All checks were successful
CI / changes (push) Successful in 6s
CI / changes (pull_request) Successful in 5s
CI / backend-tests (push) Successful in 6m23s
CI / backend-tests (pull_request) Successful in 6m22s
b24de7d3cc
lekss361 added the
GG-форсайт
scope/backend
site-finder
labels 2026-06-07 10:44:20 +00:00
bot-reviewer approved these changes 2026-06-07 10:47:21 +00:00
bot-reviewer left a comment
Collaborator

APPROVE — staff+ review (concurrency change in analyze hot path).

Two real risks verified clean (read source, not just diff):

  1. Shared-client race — NONE. EKBGeoportalClient is stateless-per-call: only self.timeout (set once in __init__, read-only after); no memoized self._keys/cache, no lazy-init, no cookie/token mutation. HTTP goes through module-level _http_get_json/_http_post_json (fresh calls), _search_keys/_urban_card build local dicts. The two concurrent calls share only self.timeout + httpx transport (thread-safe pool). Safe.
  2. Mock signature — MATCHES prod. Real get_or_fetch_zone_regulation(db, lon, lat, *, city='ekb', client=None); test mocks use (db, lon, lat, client=None) → positional order exact, not vacuous.

Session-safety invariant correct: _krt_at(client, lon, lat) is db-free (only consumer of db is _zone_regulation_at, which stays on main thread). test_session_not_shared_between_threads validates at runtime via threading.current_thread(). with ThreadPoolExecutor joins (shutdown wait=True) before exit → no leak/deadlock; graceful chain complete (_krt_at catches + future .result() catch + _zone_regulation_at catches).

Additive to #1115 (gated behind enable_ird_analyze + try/except in parcels.py) → worst case degrades to existing graceful-fail. 🟡 trivial: max_workers=1 reads counter-intuitive (one task) — cosmetic, not actionable.

Merging (squash).

<!-- gendesign-review-bot: sha=b24de7d verdict=approve --> ✅ **APPROVE** — staff+ review (concurrency change in analyze hot path). **Two real risks verified clean (read source, not just diff):** 1. **Shared-client race — NONE.** `EKBGeoportalClient` is stateless-per-call: only `self.timeout` (set once in `__init__`, read-only after); no memoized `self._keys`/cache, no lazy-init, no cookie/token mutation. HTTP goes through module-level `_http_get_json`/`_http_post_json` (fresh calls), `_search_keys`/`_urban_card` build local dicts. The two concurrent calls share only `self.timeout` + httpx transport (thread-safe pool). Safe. 2. **Mock signature — MATCHES prod.** Real `get_or_fetch_zone_regulation(db, lon, lat, *, city='ekb', client=None)`; test mocks use `(db, lon, lat, client=None)` → positional order exact, not vacuous. **Session-safety invariant correct:** `_krt_at(client, lon, lat)` is db-free (only consumer of `db` is `_zone_regulation_at`, which stays on main thread). `test_session_not_shared_between_threads` validates at runtime via `threading.current_thread()`. `with ThreadPoolExecutor` joins (shutdown wait=True) before exit → no leak/deadlock; graceful chain complete (`_krt_at` catches + future `.result()` catch + `_zone_regulation_at` catches). Additive to #1115 (gated behind enable_ird_analyze + try/except in parcels.py) → worst case degrades to existing graceful-fail. 🟡 trivial: `max_workers=1` reads counter-intuitive (one task) — cosmetic, not actionable. Merging (squash).
bot-reviewer merged commit 5b07c6641b into main 2026-06-07 10:47:26 +00:00
bot-reviewer deleted branch feat/ird-analyze-concurrent-geoportal 2026-06-07 10:47:26 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#1123
No description provided.