(high) parcels.analyze: optional DB-блоки без begin_nested() — один сбойный query отравляет всю tx, молча теряет данные и срывает persist рана #1342

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

Описание

В analyze необязательные DB-блоки (parcel_meta ~1962, egrn_block 1991, encumbrance 2058, red_lines 2084, district_price 2120, risks_block 2163, market_trend 2202, а также hydrology 1830, utilities 1880) выполняют db.execute() внутри try/except Exception БЕЗ обёртки with db.begin_nested():. Идущие следом блоки (zoning 2296, success_recommendation 2334, market_price 2389, permits 2444, velocity 2630) и persist_analysis_run — используют SAVEPOINT. Когда любой savepoint-less query падает (например PostGIS на geom::geometry при битой геометрии или временно недоступной таблице), SQLAlchemy 2.0 деактивирует транзакцию: любой следующий execute(), включая SAVEPOINT от begin_nested(), кидает PendingRollbackError. Все последующие блоки молча возвращают дефолты, а persist_analysis_run тоже падает → ран НЕ сохраняется в analysis_runs. Пользователь получает 200 с деградированным ответом без рана в истории.

Корень

Сессия из get_db() (app/core/db.py, autocommit=False, только db.close() в finally — без авто-rollback). Asymmetry: часть optional-блоков обёрнута в SAVEPOINT, часть — нет. Класс бага из .claude/rules/backend.md (no-savepoint в последовательности upsert/query).

Evidence

parcels.py:2202 (market_trend — нет begin_nested):

try:
    trend_row = db.execute(text("""WITH district_deals AS (...) ..."""), {"wkt": geom_wkt}).mappings().first()
    ...
except Exception as e:
    logger.warning("market_trend query failed for %s: %s", cad_num, e)
    market_trend = None

parcels.py:2297 (zoning — полагается на исправную tx):

try:
    with db.begin_nested():
        zoning_row = db.execute(text(""" SELECT zone_code ... """), ...)

PostGIS-векторы: red_lines (~2090) и risks_block (~2169) используют geom::geometry.

Suggested fix

Обернуть КАЖДЫЙ optional DB-блок в with db.begin_nested(): (как velocity/zoning/market_price), включая parcel_meta, egrn_block, encumbrance_block, red_lines_block, district_price_block, risks_block, market_trend, hydrology, utilities. Тогда сбой одного query откатывает только его savepoint, оставляя outer-tx рабочей для последующих блоков и persist.

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

high: данные теряются молча (отсутствие рана в history нарушает §22-контракт), триггер реалистичен (PostGIS на cast битой геометрии / временно недоступная таблица), путь достижим в проде. confidence 0.93.


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

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

  • /Users/anton/Птица/gendesign/backend/app/api/v1/parcels.py
  • /Users/anton/Птица/gendesign/backend/app/core/db.py
## Описание В `analyze` необязательные DB-блоки (parcel_meta ~1962, egrn_block 1991, encumbrance 2058, red_lines 2084, district_price 2120, risks_block 2163, market_trend 2202, а также hydrology 1830, utilities 1880) выполняют `db.execute()` внутри `try/except Exception` БЕЗ обёртки `with db.begin_nested():`. Идущие следом блоки (zoning 2296, success_recommendation 2334, market_price 2389, permits 2444, velocity 2630) и `persist_analysis_run` — используют SAVEPOINT. Когда любой savepoint-less query падает (например PostGIS на `geom::geometry` при битой геометрии или временно недоступной таблице), SQLAlchemy 2.0 деактивирует транзакцию: любой следующий `execute()`, включая `SAVEPOINT` от `begin_nested()`, кидает `PendingRollbackError`. Все последующие блоки молча возвращают дефолты, а `persist_analysis_run` тоже падает → ран НЕ сохраняется в `analysis_runs`. Пользователь получает 200 с деградированным ответом без рана в истории. ## Корень Сессия из `get_db()` (`app/core/db.py`, autocommit=False, только `db.close()` в finally — без авто-rollback). Asymmetry: часть optional-блоков обёрнута в SAVEPOINT, часть — нет. Класс бага из `.claude/rules/backend.md` (no-savepoint в последовательности upsert/query). ## Evidence `parcels.py:2202` (market_trend — нет begin_nested): ```python try: trend_row = db.execute(text("""WITH district_deals AS (...) ..."""), {"wkt": geom_wkt}).mappings().first() ... except Exception as e: logger.warning("market_trend query failed for %s: %s", cad_num, e) market_trend = None ``` `parcels.py:2297` (zoning — полагается на исправную tx): ```python try: with db.begin_nested(): zoning_row = db.execute(text(""" SELECT zone_code ... """), ...) ``` PostGIS-векторы: `red_lines` (~2090) и `risks_block` (~2169) используют `geom::geometry`. ## Suggested fix Обернуть КАЖДЫЙ optional DB-блок в `with db.begin_nested():` (как velocity/zoning/market_price), включая parcel_meta, egrn_block, encumbrance_block, red_lines_block, district_price_block, risks_block, market_trend, hydrology, utilities. Тогда сбой одного query откатывает только его savepoint, оставляя outer-tx рабочей для последующих блоков и persist. ## Обоснование severity high: данные теряются молча (отсутствие рана в history нарушает §22-контракт), триггер реалистичен (PostGIS на cast битой геометрии / временно недоступная таблица), путь достижим в проде. confidence 0.93. --- _week ревью 1 · multi-agent аудит 2026-06-14 · severity=**high** · category=`data-integrity`_ Затронутые файлы: - `/Users/anton/Птица/gendesign/backend/app/api/v1/parcels.py` - `/Users/anton/Птица/gendesign/backend/app/core/db.py`
bot-backend added the
week ревью 1
label 2026-06-14 18:08:44 +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/parcels.py (хендлер analyze_parcel, sync def на 1269). Подтверждена асимметрия SAVEPOINT-обёрток:

  • БЕЗ begin_nested: red_lines_block (2098-2122, db.execute в bare try/except, SQL c PostGIS geom::geometry ST_Intersects), risks_block (~2172-2212), market_trend (~2230-2285, exceptmarket_trend = None).
  • С begin_nested: zoning и далее — with db.begin_nested(): на 2311, 2348, 2403, 2458, 2644, 2714; persist_analysis_run(...) на 2863.

Класс бага подтверждён .claude/rules/backend.md (SAVEPOINT pattern). Сессия из get_db() (autocommit=False) — после падения savepoint-less query SQLAlchemy 2.0 деактивирует tx, и следующий begin_nested() кидает PendingRollbackError.

Корень: Часть optional-блоков analyze выполняет db.execute() без with db.begin_nested():. Сбой одного такого query (реалистичный триггер — PostGIS на cast битой геометрии в red_lines/risks) отравляет всю транзакцию → последующие блоки молча возвращают дефолты, а persist_analysis_run падает → ран не сохраняется в analysis_runs (нарушение §22-контракта истории).

Что править:

  • Обернуть КАЖДЫЙ optional DB-блок в with db.begin_nested():, начиная с red_lines_block, risks_block, market_trend (и проверить parcel_meta/egrn/encumbrance/district_price/hydrology/utilities, если в текущем main у них тоже нет savepoint). Тогда сбой query откатит только свой savepoint, оставив outer-tx рабочей для persist.

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

  • Код (на main): backend/app/api/v1/parcels.py — блоки red_lines (~2098), risks (~2172), market_trend (~2230); образец-обёртка на 2311/2348. backend/app/core/db.py (get_db, finally только close()). Тест: замокать падение red_lines query → проверить что persist_analysis_run всё равно сохраняет ран.
  • Vault: релевантных заметок не найдено.
**🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)** **Текущее состояние кода:** На `forgejo/main` `backend/app/api/v1/parcels.py` (хендлер `analyze_parcel`, sync `def` на 1269). Подтверждена асимметрия SAVEPOINT-обёрток: - БЕЗ `begin_nested`: `red_lines_block` (2098-2122, `db.execute` в bare `try/except`, SQL c PostGIS `geom::geometry` ST_Intersects), `risks_block` (~2172-2212), `market_trend` (~2230-2285, `except` → `market_trend = None`). - С `begin_nested`: zoning и далее — `with db.begin_nested():` на 2311, 2348, 2403, 2458, 2644, 2714; `persist_analysis_run(...)` на 2863. Класс бага подтверждён `.claude/rules/backend.md` (SAVEPOINT pattern). Сессия из `get_db()` (autocommit=False) — после падения savepoint-less query SQLAlchemy 2.0 деактивирует tx, и следующий `begin_nested()` кидает `PendingRollbackError`. **Корень:** Часть optional-блоков `analyze` выполняет `db.execute()` без `with db.begin_nested():`. Сбой одного такого query (реалистичный триггер — PostGIS на cast битой геометрии в red_lines/risks) отравляет всю транзакцию → последующие блоки молча возвращают дефолты, а `persist_analysis_run` падает → ран не сохраняется в `analysis_runs` (нарушение §22-контракта истории). **Что править:** - Обернуть КАЖДЫЙ optional DB-блок в `with db.begin_nested():`, начиная с `red_lines_block`, `risks_block`, `market_trend` (и проверить parcel_meta/egrn/encumbrance/district_price/hydrology/utilities, если в текущем main у них тоже нет savepoint). Тогда сбой query откатит только свой savepoint, оставив outer-tx рабочей для persist. **Что посмотреть:** - Код (на main): `backend/app/api/v1/parcels.py` — блоки red_lines (~2098), risks (~2172), market_trend (~2230); образец-обёртка на 2311/2348. `backend/app/core/db.py` (`get_db`, finally только `close()`). Тест: замокать падение red_lines query → проверить что persist_analysis_run всё равно сохраняет ран. - 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#1342
No description provided.