(high) Синхронный CPU-bound генеративный пайплайн geometry.generate блокирует event loop в create_concept #1338

Closed
opened 2026-06-14 18:08:39 +00:00 by bot-backend · 1 comment
Collaborator

Описание

async def create_concept напрямую вызывает синхронный тяжёлый CPU-bound пайплайн geometry.generate(payload) без offload. Пайплайн парсит/репроецирует Shapely, строит сетку до MAX_GRID_CELLS=20_000 ячеек и для каждой из 3 стратегий делает жадную раскладку с перестройкой STRtree на каждое размещение (квадратичная сложность). Пока считается крупный участок, event loop воркера полностью заблокирован — health-чеки, другие API-запросы и WebSocket-сообщения на этом воркере стоят в очереди.

Корень

Блокирующий CPU-bound вызов внутри async def без run_in_threadpool/asyncio.to_thread. Роут зарегистрирован безусловно в проде (main.py:163, prefix /api/v1/concepts) без feature-флага. Соседние async-хендлеры репозитория (chat.py, llm/client.py) уже используют threadpool-обёртки — здесь её забыли. Нарушение .claude/rules/backend.md.

Evidence

concepts.py:15,26:

async def create_concept(payload: ConceptInput) -> ConceptOutput:
    ...
    variants = geometry.generate(payload)   # синхронный CPU-bound вызов в event loop

placement.py:123-151 (перестройка индекса O(n) на каждое размещение → O(n^2)):

for cell in parcel.grid:
    ...
    placed.append(footprint)
    buffered.append(candidate_buf)
    tree = STRtree(buffered)   # rebuild на КАЖДОЕ размещение

geometry.generate() (geometry.py:294-307) — plain sync def, без async/executor в теле и цепочке.

Suggested fix

from starlette.concurrency import run_in_threadpool; variants = await run_in_threadpool(geometry.generate, payload) (или await asyncio.to_thread(geometry.generate, payload)) — совпадёт с принятым в chat.py/llm паттерном. Дополнительно (оптимизация, не корректность): батчить перестройку STRtree вместо rebuild на каждое размещение.

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

high: один POST /api/v1/concepts для нетривиального участка блокирует event loop воркера на всю длительность (секунды). Нет смягчающих факторов (флаг/guard/unreachable отсутствуют). confidence 0.97.


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

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

  • /Users/anton/Птица/gendesign/backend/app/services/generative/geometry.py
  • /Users/anton/Птица/gendesign/backend/app/api/v1/concepts.py
  • /Users/anton/Птица/gendesign/backend/app/services/generative/placement.py
## Описание `async def create_concept` напрямую вызывает синхронный тяжёлый CPU-bound пайплайн `geometry.generate(payload)` без offload. Пайплайн парсит/репроецирует Shapely, строит сетку до `MAX_GRID_CELLS=20_000` ячеек и для каждой из 3 стратегий делает жадную раскладку с перестройкой STRtree на каждое размещение (квадратичная сложность). Пока считается крупный участок, event loop воркера полностью заблокирован — health-чеки, другие API-запросы и WebSocket-сообщения на этом воркере стоят в очереди. ## Корень Блокирующий CPU-bound вызов внутри `async def` без `run_in_threadpool`/`asyncio.to_thread`. Роут зарегистрирован безусловно в проде (`main.py:163`, prefix `/api/v1/concepts`) без feature-флага. Соседние async-хендлеры репозитория (chat.py, llm/client.py) уже используют threadpool-обёртки — здесь её забыли. Нарушение `.claude/rules/backend.md`. ## Evidence `concepts.py:15,26`: ```python async def create_concept(payload: ConceptInput) -> ConceptOutput: ... variants = geometry.generate(payload) # синхронный CPU-bound вызов в event loop ``` `placement.py:123-151` (перестройка индекса O(n) на каждое размещение → O(n^2)): ```python for cell in parcel.grid: ... placed.append(footprint) buffered.append(candidate_buf) tree = STRtree(buffered) # rebuild на КАЖДОЕ размещение ``` `geometry.generate()` (geometry.py:294-307) — plain sync `def`, без async/executor в теле и цепочке. ## Suggested fix `from starlette.concurrency import run_in_threadpool; variants = await run_in_threadpool(geometry.generate, payload)` (или `await asyncio.to_thread(geometry.generate, payload)`) — совпадёт с принятым в chat.py/llm паттерном. Дополнительно (оптимизация, не корректность): батчить перестройку STRtree вместо rebuild на каждое размещение. ## Обоснование severity high: один `POST /api/v1/concepts` для нетривиального участка блокирует event loop воркера на всю длительность (секунды). Нет смягчающих факторов (флаг/guard/unreachable отсутствуют). confidence 0.97. --- _week ревью 1 · multi-agent аудит 2026-06-14 · severity=**high** · category=`async-concurrency`_ Затронутые файлы: - `/Users/anton/Птица/gendesign/backend/app/services/generative/geometry.py` - `/Users/anton/Птица/gendesign/backend/app/api/v1/concepts.py` - `/Users/anton/Птица/gendesign/backend/app/services/generative/placement.py`
bot-backend added the
week ревью 1
label 2026-06-14 18:08:39 +00:00
bot-backend added the
scope/backend
label 2026-06-15 11:01:49 +00:00
Owner

🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)

Текущее состояние кода: На forgejo/main (канон прод) backend/app/api/v1/concepts.py:14-30async def create_concept напрямую вызывает синхронный geometry.generate(payload) (строка 26) без offload в threadpool. geometry.py:294 def generate(...) — plain sync, MAX_GRID_CELLS = 20_000 (geometry.py:54). placement.py:123-151 перестраивает STRtree(buffered) на КАЖДОЕ размещение (строка 151, в цикле for cell in parcel.grid) — O(n²). Роут зарегистрирован безусловно: main.py:162 include_router(concepts.router, prefix="/api/v1/concepts"), без feature-флага.

(NB: в текущей рабочей ветке feat/cian-city-sweep-schedule concepts.py — Stage-1a stub generate_stub; баг присутствует именно на forgejo/main, куда метит issue.)

Корень: Блокирующий CPU-bound вызов geometry.generate внутри async def без run_in_threadpool/asyncio.to_thread → event loop воркера заблокирован на всю длительность раскладки крупного участка.

Что править:

  • concepts.py:26: from starlette.concurrency import run_in_threadpoolvariants = await run_in_threadpool(geometry.generate, payload) (паттерн как в chat.py/llm). Обработку ParcelGeometryError сохранить вокруг await.
  • (Опционально, perf, не корректность) батчить перестройку STRtree в placement.py вместо rebuild на каждое размещение.

Что посмотреть:

  • Код: backend/app/api/v1/concepts.py:26, backend/app/services/generative/geometry.py:294, backend/app/services/generative/placement.py:123-151. Тест: убедиться что хендлер не блокирует loop (async-смоук / проверка что generate вызывается в threadpool).
  • Vault: релевантных заметок не найдено.
**🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)** **Текущее состояние кода:** На `forgejo/main` (канон прод) `backend/app/api/v1/concepts.py:14-30` — `async def create_concept` напрямую вызывает синхронный `geometry.generate(payload)` (строка 26) без offload в threadpool. `geometry.py:294` `def generate(...)` — plain sync, `MAX_GRID_CELLS = 20_000` (geometry.py:54). `placement.py:123-151` перестраивает `STRtree(buffered)` на КАЖДОЕ размещение (строка 151, в цикле `for cell in parcel.grid`) — O(n²). Роут зарегистрирован безусловно: `main.py:162` `include_router(concepts.router, prefix="/api/v1/concepts")`, без feature-флага. (NB: в текущей рабочей ветке `feat/cian-city-sweep-schedule` concepts.py — Stage-1a stub `generate_stub`; баг присутствует именно на `forgejo/main`, куда метит issue.) **Корень:** Блокирующий CPU-bound вызов `geometry.generate` внутри `async def` без `run_in_threadpool`/`asyncio.to_thread` → event loop воркера заблокирован на всю длительность раскладки крупного участка. **Что править:** - `concepts.py:26`: `from starlette.concurrency import run_in_threadpool` → `variants = await run_in_threadpool(geometry.generate, payload)` (паттерн как в chat.py/llm). Обработку `ParcelGeometryError` сохранить вокруг await. - (Опционально, perf, не корректность) батчить перестройку STRtree в `placement.py` вместо rebuild на каждое размещение. **Что посмотреть:** - Код: `backend/app/api/v1/concepts.py:26`, `backend/app/services/generative/geometry.py:294`, `backend/app/services/generative/placement.py:123-151`. Тест: убедиться что хендлер не блокирует loop (async-смоук / проверка что generate вызывается в threadpool). - Vault: релевантных заметок не найдено.
Sign in to join this conversation.
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#1338
No description provided.