fix(site-finder): cascade InFailedSqlTransaction — velocity internal rollback #155
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#155
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/velocity-internal-rollback-cascade"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Root cause (final)
POST /analyze всё ещё крашится 500 после PR #154 SAVEPOINT wrap. Глубже investigation:
velocity.pyимеет 3 internal try/except (lines 77, 149, 268) которые:Из-за
return Noneexception НЕ propagates за пределы velocity. PR #154 SAVEPOINT не triggers rollback (no exception to catch). Но Postgres transaction УЖЕ aborted внутри savepoint. Caller (analyze_parcel) видитvelocity = Noneнормально, но следующая SQL query (geotech_risk → osm_noise_sources_ekb) → cascadeInFailedSqlTransaction.Fix
velocity.py — db.rollback() в 3 catch блоках
После logger.exception/warning делаем
db.rollback()(с try/except защитой на double-rollback case).parcels.py — SAVEPOINT для zoning + success_recommendation
Defensive — те же try/except без rollback patterns. Wrap в
with db.begin_nested().Test plan
После merge → deploy → POST /analyze работает (velocity gracefully degrades к None, остальные fields normal).
Related
PR #154 был partial — fixed SAVEPOINT wrap но не internal velocity error swallow.
Refs: user reports cascade InFailedSqlTransaction
Bot review (SHA
c3cafef, CI ✅)Follow-up к моему PR #154 ревью. Author более глубоко investigated и нашёл что #154 был partial.
Корректный root cause analysis
✅ velocity.py 3 internal try/except swallow'или exception:
return Noneбезdb.rollback(). PR #154's outer SAVEPOINT не triggers rollback потому что no exception propagates.✅ Postgres tx уже aborted внутри savepoint, но savepoint commit'нулся cleanly → outer tx остаётся aborted → next query (
_geotech_riskline 828) → cascadeInFailedSqlTransaction.Fix scope
✅
velocity.py: 3 catch блока теперь делаютtry: db.rollback() except: passпосле log + перед return None. Double-protection (inner try) защищает от double-rollback edge cases.✅
parcels.py: zoning + success_recommendation queries имели похожий "try/except no rollback" anti-pattern — wrapped вwith db.begin_nested():defensively.✅ CI green, +76/-57.
Subtle concern (запросил cross-check от агента)
db.rollback()внутриcompute_velocityвызывается из caller'а который обернул вызов вwith db.begin_nested():(PR #154). Теоретическая question: SQLAlchemy 2.0Session.rollback()semantics inside savepoint context — rolls back savepoint only или entire outer tx?Если entire tx → savepoint released → caller's
with db.begin_nested():exit может крашиться trying to commit released savepoint.Empirically CI passes, double-try protects от rollback() exceptions. Но это subtle. Запросил независимое мнение через
general-purposeагента — обновлю если найдёт что-то.Лучший альтернативный fix (рассмотреть в follow-up)
Cleaner был бы: убрать internal try/except в velocity, let exceptions propagate. Caller's
with db.begin_nested():тогда автоматически roll-back-нёт savepoint + transaction state clean. Меньше surface, меньше edge cases.Но текущий fix должен работать т.к. double-try в velocity защищает от любых rollback issues. Принимаю.
Минор
parcels.pylines 1693-1735 имеют огромный indent shift — это простоwithwrap, content идентичен. Diff визуально шумный, но логически тривиальный.Жду агент
Если он подтвердит concerns про savepoint semantics — обновлю review. Иначе approve+merge на следующем тике.
⚠️ UPDATE: Bot review — cross-checked, fix incorrect
После независимого cross-check'а через
general-purposeagent (анализ SQLAlchemy 2.0 semantics) — этот PR не нужно мерджить как есть, он вводит худший bug чем решает.Root cause проблемы fix'а
db.rollback()внутриcompute_velocityвызывается из caller'а который обернул вызов вwith db.begin_nested():(PR #154). И тут крайне subtle SQLAlchemy 2.0 поведение:Session.rollback()rolls back ENTIRE outer transaction, не savepoint. SQLAlchemy pops all SAVEPOINTs и rollback'ает root. Чтобы откатить только savepoint, нужно позволить context manager сделать это через exception propagation (что и происходит приwith begin_nested():+ raise).Когда velocity catch вызывает
db.rollback():InFailedSqlTransaction)with db.begin_nested():savepoint становится orphaned — SessionTransaction больше не active__exit__SQLAlchemy пытаетсяRELEASE SAVEPOINTна already-deactivated tx → raisesResourceClosedError/InvalidRequestError: This SessionTransaction is no longer activeNet effect: даже если caller's outer try/except swallow'ит это, все последующие queries в analyze_parcel выполняются вне tx или в broken state (root tx был nuked under them). Получается тот же класс bugs, который PR пытается решить, только тоньше.
CI green только потому что savepoint-orphan path не покрыт тестами — unit tests мockирующие Session не triggers реальный SAVEPOINT lifecycle. Prod упадёт на первом velocity SQL exception.
Правильный fix
Mirror тот pattern, который PR #155 уже применил правильно для zoning + success_recommendation queries в parcels.py — обернуть каждый
db.execute(...)вvelocity.pyв свойwith db.begin_nested()::Заменить так все 3 catch блока в velocity.py (lines ~126, ~193, ~323), убрать
db.rollback()calls.Альтернатива (option B, проще): убрать internal try/except из velocity.py вообще, let exceptions propagate. Caller's
with db.begin_nested():(из PR #154) автоматически handle'ит. Меньше surface, меньше edge cases. Но теряем custom log message внутри velocity.Recommendation
Request changes:
db.rollback()блока вvelocity.pyнаwith db.begin_nested():wrappers (или option B — убрать internal try/except)parcels.pyzoning + success_recommendation changes — OK, паттерн правильныйИзвинения
Я в interim review (15 min назад) предположил "double-try защищает, CI green, accept". Это было слишком оптимистично — агент справедливо разобрал SQLAlchemy semantics глубже, я этот edge case недостаточно проверил.
Это именно тот тип subtle tx-state bug которым стоит спорить с CI green'ом. Не мерджу.
Final review (SHA
37fd4b5, CI ✅)Cross-checked через
general-purposeagent — fix полностью корректен и адресует bug из моего предыдущего review.Что проверено агентом
✅
velocity.py: все 3db.rollback()удалены, каждыйdb.execute(...)wrapped вwith db.begin_nested():per-query✅ Pattern correctness: try →
with begin_nested():→ execute → savepoint__exit__auto-rollback ТОЛЬКО savepoint на exception → outer tx clean →return None✅
parcels.py: zoning + success_recommendation wrappers preserved✅ Inline comments документируют SQLAlchemy 2.0 SAVEPOINT semantics (ссылка на bot review)
✅ No side-effects: control flow (graceful
return None) сохранёнМинор (не блокер)
Нет regression test для tx-cascade scenario — был бы ценен (mock outer SAVEPOINT + failing inner query → проверить что caller's tx остаётся clean), но не blocker для этого fix'а.
CI
approve merge