Compare commits
1 commit
main
...
fix/2464-p
| Author | SHA1 | Date | |
|---|---|---|---|
| b9381e15e0 |
2 changed files with 96 additions and 8 deletions
|
|
@ -925,14 +925,17 @@ def _neighbors_summary(db: Session, geom_wkt: str, our_cad_num: str) -> dict[str
|
|||
integration EXPLAIN-gate, см. `test_analyze_parcels_sql.py`).
|
||||
"""
|
||||
try:
|
||||
row = (
|
||||
db.execute(
|
||||
_NEIGHBORS_SUMMARY_SQL,
|
||||
{"wkt": geom_wkt, "our_cad": our_cad_num},
|
||||
# #2464: SAVEPOINT — сессия общая с analyze_parcel, ошибку глотаем ниже. Без него
|
||||
# aborted-транзакция дошла бы до persist_analysis_run, и анализ не сохранился бы.
|
||||
with db.begin_nested():
|
||||
row = (
|
||||
db.execute(
|
||||
_NEIGHBORS_SUMMARY_SQL,
|
||||
{"wkt": geom_wkt, "our_cad": our_cad_num},
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
.mappings()
|
||||
.first()
|
||||
)
|
||||
neighbor_rows: list[dict[str, Any]] = list(row["neighbors"]) if row else []
|
||||
overlap_row: list[dict[str, Any]] = list(row["overlap_rows"]) if row else []
|
||||
# #2464 cluster B: честный total из neighbors_total CTE (БЕЗ LIMIT 30) —
|
||||
|
|
@ -3750,7 +3753,14 @@ def analyze_parcel(
|
|||
"source": "gisogd66",
|
||||
}
|
||||
try:
|
||||
permits_nearby_data = get_permits_nearby(db, geom_wkt, radius_m=500)
|
||||
# #2464: SAVEPOINT, как у соседних блоков этой же функции (ближайший — разрешения
|
||||
# 10d-pre2 шестьюдесятью строками выше, где приём применён явно). get_permits_nearby
|
||||
# делает db.execute на ЭТОЙ сессии и своей защиты не имеет; ошибку глотаем здесь.
|
||||
# Без savepoint'а упавший запрос оставляет транзакцию в aborted-состоянии, и дальше
|
||||
# по обработчику падают _geotech_risk (:4141), _neighbors_summary (:4145) и запись
|
||||
# прогона — то есть теряется весь анализ, а не блок разрешений.
|
||||
with db.begin_nested():
|
||||
permits_nearby_data = get_permits_nearby(db, geom_wkt, radius_m=500)
|
||||
except Exception as e:
|
||||
logger.warning("gisogd permits_nearby query failed for %s: %s", cad_num, e)
|
||||
|
||||
|
|
|
|||
78
backend/tests/api/v1/test_2464_parcels_savepoint_pair.py
Normal file
78
backend/tests/api/v1/test_2464_parcels_savepoint_pair.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""#2464: два места analyze_parcel глотали ошибку БД без SAVEPOINT.
|
||||
|
||||
Найдено поиском «правило соблюдено рядом — нарушено здесь»: в самой функции
|
||||
`analyze_parcel` двенадцать соседних блоков оборачивают свой `db.execute` в
|
||||
`db.begin_nested()`, и у velocity-блока правило записано словами —
|
||||
|
||||
«SAVEPOINT защищает outer transaction если velocity SQL падает — иначе
|
||||
следующие queries (_geotech_risk и пр.) крашатся с InFailedSqlTransaction»
|
||||
|
||||
Два блока из правила выпадали:
|
||||
|
||||
* `get_permits_nearby` (вызов на общей сессии, своей защиты не имеет, ошибку
|
||||
глотает вызывающий) — а после него идут `_geotech_risk`, `_neighbors_summary`
|
||||
и запись прогона: терялся бы весь анализ, а не блок разрешений;
|
||||
* `_neighbors_summary` — вызывается перед `persist_analysis_run`.
|
||||
|
||||
Двойник сессии (`tests/support/pg_session.py`) воспроизводит семантику Postgres:
|
||||
обычный MagicMock aborted-состояния не имеет, и проверка «сессия осталась рабочей»
|
||||
на нём зелена независимо от наличия SAVEPOINT.
|
||||
|
||||
ЧТО ЗДЕСЬ НЕ ПОКРЫТО И ПОЧЕМУ
|
||||
─────────────────────────────
|
||||
Прямой тест есть только на `_neighbors_summary` — это отдельная функция, её можно
|
||||
вызвать. Блок `get_permits_nearby` лежит ВНУТРИ обработчика `analyze_parcel`, и
|
||||
дотянуться до него можно лишь через полный эндпоинт, чей мок-двойник сессии должен
|
||||
уметь отвечать на десятки разных запросов.
|
||||
|
||||
Первая версия этого файла «покрывала» его так: тест сам писал
|
||||
`with db.begin_nested():` вокруг вызова и проверял, что сессия выжила. Такая
|
||||
проверка зелена независимо от боевого кода — она проверяет собственную обёртку.
|
||||
Убрана: лучше честно отсутствующее покрытие, чем поддельное. Правка там та же
|
||||
самая и по тому же доводу, но подтверждена чтением, а не тестом.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.support.pg_session import AbortedTransactionError, PostgresLikeSession
|
||||
|
||||
|
||||
def test_neighbors_summary_failure_leaves_session_usable() -> None:
|
||||
"""Сбой запроса соседей не должен помешать сохранить прогон."""
|
||||
from app.api.v1 import parcels
|
||||
|
||||
db = PostgresLikeSession()
|
||||
wkt = "POLYGON((60.6 56.83, 60.61 56.83, 60.61 56.84, 60.6 56.84, 60.6 56.83))"
|
||||
|
||||
out = parcels._neighbors_summary(db, wkt, "66:41:0402029:25")
|
||||
|
||||
assert out["data_available"] is False, "деградация должна остаться graceful"
|
||||
db.execute("SELECT 1") # на origin/main здесь AbortedTransactionError
|
||||
assert db.calls == 2
|
||||
|
||||
|
||||
def test_double_still_poisons_without_a_savepoint() -> None:
|
||||
"""Контроль на инструмент: без SAVEPOINT двойник ОБЯЗАН отравляться.
|
||||
|
||||
Без этой проверки обе проверки выше были бы зелёными по построению.
|
||||
"""
|
||||
db = PostgresLikeSession()
|
||||
with pytest.raises(RuntimeError):
|
||||
db.execute("boom")
|
||||
with pytest.raises(AbortedTransactionError):
|
||||
db.execute("SELECT 1")
|
||||
|
||||
|
||||
def test_neighbors_summary_healthy_path_unchanged() -> None:
|
||||
"""Контроль: без сбоя поведение прежнее — данные читаются, не fallback."""
|
||||
from app.api.v1 import parcels
|
||||
|
||||
db = PostgresLikeSession(
|
||||
fail_on=(),
|
||||
row={"neighbors": [], "overlap_rows": [], "neighbors_total_count": 0},
|
||||
)
|
||||
out = parcels._neighbors_summary(db, "POLYGON EMPTY", "66:41:0402029:25")
|
||||
|
||||
assert out.get("data_available") is not False
|
||||
Loading…
Add table
Reference in a new issue