fix(forecasting): SAVEPOINT-isolate §22 db.execute failures + close RELEASE-trap (#2464) #2466

Merged
bot-backend merged 1 commit from worktree-agent-a32af8863ce3aefd0 into main 2026-07-08 05:41:36 +00:00
Collaborator

Что

Волна 1 эпика #2464 — подкластер A: session-poisoning в §22-прогнозе (forecasting) + второй порядок этой же ловушки.

Баг-класс

§22-отчёт делит одну SQLAlchemy-сессию между всеми builder'ами. Несколько ловили сбой db.execute, логировали и возвращали фолбэк ({}/[]/None) без rollback → Postgres оставляет транзакцию aborted → каждый следующий builder на той же сессии падает current transaction is aborted, молча обнуляя все последующие слои (вопреки докстрингам «сбой одного ряда не валит остальные»).

Фикс (SAVEPOINT with db.begin_nested(): на самом swallow-сайте)

Файл Сайт Гранулярность
orchestrator.py _safe_call (9 §9.x-слоёв) вокруг всего fn() (потребовало явный db: Session-параметр + правку 9 call-site'ов)
macro_series.py _query_key_rate_monthly / _query_inflation_monthly / _query_mortgage_monthly per-индикатор
special_indices.py _run (6 §25-индексов) вокруг каждого builder()
sales_series.py _query_source_a / _query_source_b per-запрос

Второй порядок — ловушка RELEASE SAVEPOINT

Внешний savepoint не спасает, если внутренний хелпер, вызванный внутри with db.begin_nested(): return builder(), глотает свою ошибку db.execute без своего savepoint: транзакция уходит в aborted, внешний with на нормальном выходе делает RELEASE SAVEPOINT, а RELEASE в aborted-состоянии сам падает (Postgres разрешает только ROLLBACK / ROLLBACK TO SAVEPOINT) → внешний except ловит, но откат так и не происходит → poisoning каскадит. Воспроизведено на реальном Postgres 16. Фикс — savepoint на внутреннем catch-сайте:

  • special_indices.py _query_parcel_centroid (ST_Centroid)
  • site_finder/district_resolver.py _admin_names + resolve_objective_districts (внутренние хелперы, отравлявшие forecasting-builder'ы)

Тесты

20 новых. Ключевое — _ReleaseTrapSession-фейк моделирует все три семантики Postgres: SAVEPOINT запрещён на входе в aborted-tx, ROLLBACK TO SAVEPOINT чистит poison на exception-выходе, и RELEASE падает на нормальном выходе при poisoned-tx (сама ловушка). Каждый тест оборачивает внутренний хелпер в имитацию _run/_safe_call, роняет db.execute один раз → проверяет, что вернулся фолбэк самого хелпера (не внешний — доказывает, что RELEASE не выстрелил) и что следующий db.execute на той же сессии успешен. Red/green: реверт 3 фиксов → 3 trap-теста красные; восстановление → зелёные. tests/services/forecasting/ + tests/services/site_finder/: 1643 passed; полный сьют ранее 4354 passed.

Ревью

code-reviewer: APPROVE (финальный, после дельты). Сам перепрогнал red/green на scratch-копии; подтвердил корректность shape SAVEPOINT, безопасность cache-write в _admin_names (на сбое кэш не отравляется), владение сессией во всех call-site'ах district_resolver, отсутствие :x::type, отсутствие tradein/миграций.

Refs #2464 (cluster A).

## Что Волна 1 эпика **#2464** — подкластер **A: session-poisoning** в §22-прогнозе (forecasting) + второй порядок этой же ловушки. ### Баг-класс §22-отчёт делит **одну** SQLAlchemy-сессию между всеми builder'ами. Несколько ловили сбой `db.execute`, логировали и возвращали фолбэк (`{}`/`[]`/`None`) **без rollback** → Postgres оставляет транзакцию `aborted` → каждый следующий builder на той же сессии падает `current transaction is aborted`, молча обнуляя все последующие слои (вопреки докстрингам «сбой одного ряда не валит остальные»). ### Фикс (SAVEPOINT `with db.begin_nested():` на самом swallow-сайте) | Файл | Сайт | Гранулярность | |---|---|---| | `orchestrator.py` | `_safe_call` (9 §9.x-слоёв) | вокруг всего `fn()` (потребовало явный `db: Session`-параметр + правку 9 call-site'ов) | | `macro_series.py` | `_query_key_rate_monthly` / `_query_inflation_monthly` / `_query_mortgage_monthly` | per-индикатор | | `special_indices.py` | `_run` (6 §25-индексов) | вокруг каждого `builder()` | | `sales_series.py` | `_query_source_a` / `_query_source_b` | per-запрос | ### Второй порядок — ловушка RELEASE SAVEPOINT Внешний savepoint **не спасает**, если внутренний хелпер, вызванный внутри `with db.begin_nested(): return builder()`, глотает свою ошибку `db.execute` без **своего** savepoint: транзакция уходит в aborted, внешний `with` на нормальном выходе делает `RELEASE SAVEPOINT`, а RELEASE в aborted-состоянии сам падает (Postgres разрешает только ROLLBACK / ROLLBACK TO SAVEPOINT) → внешний `except` ловит, но откат так и не происходит → poisoning каскадит. Воспроизведено на реальном Postgres 16. Фикс — savepoint на **внутреннем** catch-сайте: - `special_indices.py` `_query_parcel_centroid` (ST_Centroid) - `site_finder/district_resolver.py` `_admin_names` + `resolve_objective_districts` (внутренние хелперы, отравлявшие forecasting-builder'ы) ## Тесты 20 новых. Ключевое — `_ReleaseTrapSession`-фейк моделирует **все три** семантики Postgres: SAVEPOINT запрещён на входе в aborted-tx, ROLLBACK TO SAVEPOINT чистит poison на exception-выходе, и **RELEASE падает на нормальном выходе при poisoned-tx** (сама ловушка). Каждый тест оборачивает внутренний хелпер в имитацию `_run`/`_safe_call`, роняет `db.execute` один раз → проверяет, что вернулся фолбэк **самого хелпера** (не внешний — доказывает, что RELEASE не выстрелил) и что следующий `db.execute` на той же сессии успешен. Red/green: реверт 3 фиксов → 3 trap-теста красные; восстановление → зелёные. `tests/services/forecasting/` + `tests/services/site_finder/`: 1643 passed; полный сьют ранее 4354 passed. ## Ревью code-reviewer: **✅ APPROVE** (финальный, после дельты). Сам перепрогнал red/green на scratch-копии; подтвердил корректность shape SAVEPOINT, безопасность cache-write в `_admin_names` (на сбое кэш не отравляется), владение сессией во всех call-site'ах district_resolver, отсутствие `:x::type`, отсутствие tradein/миграций. Refs #2464 (cluster A).
bot-backend added 1 commit 2026-07-07 12:50:58 +00:00
fix(forecasting): savepoint db.execute failures to stop §22 session poisoning (#2464)
All checks were successful
CI / changes (pull_request) Successful in 11s
CI Trade-In / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 3m1s
CI / backend-tests (pull_request) Successful in 16m4s
3a13bce468
One layer's caught DB error left the shared §22 report Session in Postgres's
"aborted transaction" state with no rollback, silently emptying every later
layer on the same report. Wrap each failure point in db.begin_nested()
(SAVEPOINT): orchestrator._safe_call, macro_series _query_key_rate_monthly /
_query_inflation_monthly / _query_mortgage_monthly (per indicator),
special_indices._run (per index), and sales_series _query_source_a /
_query_source_b — so a sibling builder's failure no longer cascades.

Also close the RELEASE-SAVEPOINT trap: inner helpers that swallowed a
db.execute failure without their own savepoint left the tx aborted, so the
outer builder's savepoint failed at RELEASE (illegal in an aborted tx) and
poisoning still cascaded. Add savepoints at the inner catch sites —
special_indices._query_parcel_centroid and district_resolver._admin_names /
resolve_objective_districts.

Refs #2464 (cluster A session-poisoning).
bot-backend merged commit 82fdabccba into main 2026-07-08 05:41:36 +00:00
bot-backend deleted branch worktree-agent-a32af8863ce3aefd0 2026-07-08 05:41:37 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#2466
No description provided.