fix(site-finder): cascade InFailedSqlTransaction — velocity internal rollback #155

Merged
lekss361 merged 2 commits from fix/velocity-internal-rollback-cascade into main 2026-05-15 05:52:39 +00:00
Showing only changes of commit 37fd4b5a8e - Show all commits

View file

@ -74,11 +74,15 @@ def compute_velocity(
# obj_class в domrf_kn_objects заполнен слабо (много NULL); фильтруем
# только если явно передан.
class_filter = "AND o.obj_class = :obj_class" if obj_class else ""
# SAVEPOINT per query: failure rollbacks ТОЛЬКО savepoint, не outer tx.
# db.rollback() здесь НЕЛЬЗЯ — он orphan'ит outer SessionTransaction
# (см. PR #155 bot review — SQLAlchemy 2.0 begin_nested context cleanup).
try:
comp_rows = (
db.execute(
text(
f"""
with db.begin_nested():
comp_rows = (
db.execute(
text(
f"""
WITH latest_obj AS (
SELECT DISTINCT ON (obj_id)
obj_id,
@ -114,25 +118,20 @@ def compute_velocity(
ORDER BY distance_m ASC
LIMIT 200
"""
),
{
"parcel_wkt": parcel_geom_wkt,
"radius_m": radius_km * 1000.0,
"obj_class": obj_class,
},
),
{
"parcel_wkt": parcel_geom_wkt,
"radius_m": radius_km * 1000.0,
"obj_class": obj_class,
},
)
.mappings()
.all()
)
.mappings()
.all()
)
except Exception:
logger.exception("velocity: competitor query failed for wkt=%s", parcel_geom_wkt[:80])
# КРИТИЧНО: rollback aborted transaction чтобы caller (analyze_parcel)
# мог продолжить выполнять последующие queries. Иначе caller получает
# cascade InFailedSqlTransaction на следующей SQL execute.
try:
db.rollback()
except Exception:
pass
# SAVEPOINT auto-rollbacks через __exit__ context manager.
# Outer tx остаётся clean — caller продолжает работать без cascade.
return None
if not comp_rows:
@ -154,10 +153,11 @@ def compute_velocity(
# area_sq = м² за месяц (primary). Если NULL — realised * 45 м² heuristic.
# type = 'apartments' — только жильё.
try:
sales_rows = (
db.execute(
text(
"""
with db.begin_nested():
sales_rows = (
db.execute(
text(
"""
WITH latest_sg AS (
SELECT DISTINCT ON (obj_id, report_month)
obj_id,
@ -182,22 +182,18 @@ def compute_velocity(
WHERE area_sq > 0 OR realised > 0
GROUP BY obj_id
"""
),
{
"obj_ids": obj_ids,
"window_interval": f"{months_window} months",
},
),
{
"obj_ids": obj_ids,
"window_interval": f"{months_window} months",
},
)
.mappings()
.all()
)
.mappings()
.all()
)
except Exception:
logger.exception("velocity: sale_graph query failed for obj_ids=%s", obj_ids[:5])
# КРИТИЧНО: rollback aborted transaction (см. competitor query комментарий)
try:
db.rollback()
except Exception:
pass
# SAVEPOINT auto-rollback'нут — outer tx clean
return None
if not sales_rows:
@ -278,10 +274,11 @@ def _get_ekb_median(db: Session, months_window: int = 6) -> float | None:
Fallback к _EKB_MEDIAN_FALLBACK_SQM_PER_MONTH если нет данных в БД.
"""
try:
row = (
db.execute(
text(
"""
with db.begin_nested():
row = (
db.execute(
text(
"""
WITH latest_sg AS (
SELECT DISTINCT ON (obj_id, report_month)
obj_id,
@ -315,19 +312,15 @@ def _get_ekb_median(db: Session, months_window: int = 6) -> float | None:
SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY velocity) AS median
FROM per_obj_velocity
"""
),
{"window_interval": f"{months_window} months"},
),
{"window_interval": f"{months_window} months"},
)
.mappings()
.first()
)
.mappings()
.first()
)
except Exception:
logger.warning("velocity: ekb_median query failed, using fallback")
# КРИТИЧНО: rollback aborted transaction
try:
db.rollback()
except Exception:
pass
# SAVEPOINT auto-rollback'нут
return None
if row and row["median"] is not None: