(high) poi_loader.sync_poi_to_db: UPSERT-цикл без per-row SAVEPOINT — одна битая строка валит весь weekly POI-sync #1343

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

Описание

Цикл INSERT/UPSERT в osm_poi_ekb выполняет db.execute(...) напрямую, без with db.begin_nested(): per-row, в отличие от соседнего noise_loader.sync_noise_sources_to_db (там SAVEPOINT есть, noise_loader.py:247-285). На psycopg3/SQLAlchemy любой сбой одного INSERT (битый tag-jsonb, нарушение constraint, кодировка, PostGIS-ошибка у way/relation без корректного center) переводит транзакцию в aborted-состояние; все последующие db.execute в цикле падают с current transaction is aborted, управление улетает в outer exceptdb.rollback() откатывает ВСЮ загрузку. Один дефектный OSM-элемент обнуляет весь еженедельный POI-sync.

Корень

Нарушено документированное правило проекта .claude/rules/backend.md (SAVEPOINT pattern) и vault Bug_Pzz_Loader_Missing_Savepoint. Контрпример — соседний noise_loader.py — уже использует with db.begin_nested(): с явным комментарием об этой же проблеме.

Evidence

poi_loader.py:124-191 — цикл for el in elements: с голым execute (строки ~158-186):

result = db.execute(
    text("""INSERT INTO osm_poi_ekb ... ON CONFLICT (osm_type, osm_id, category) DO UPDATE ... RETURNING (xmax = 0) AS is_insert"""),
    {...},
).scalar()  # ← нет 'with db.begin_nested():' вокруг

Внешний except Exception (~194) вызывает db.rollback().

Suggested fix

Обернуть каждый INSERT в SAVEPOINT и считать сбой как skipped (по образцу noise_loader.py:247-285):

for el in elements:
    try:
        with db.begin_nested():
            result = db.execute(text("INSERT ... RETURNING (xmax = 0) AS is_insert"), {...}).scalar()
        inserted += 1 if result else 0
        updated += 0 if result else 1
    except Exception as e:
        logger.warning("poi_sync insert failed for %s/%s: %s", osm_type, osm_id, e)
        skipped += 1

Затем один db.commit() в конце.

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

high: потеря всего еженедельного POI-sync из-за одного дефектного OSM-элемента; путь достижим (weekly Celery beat, реальные OSM-данные). confidence 0.97.


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

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

  • /Users/anton/Птица/gendesign/backend/app/services/site_finder/poi_loader.py
  • /Users/anton/Птица/gendesign/backend/app/services/site_finder/noise_loader.py
## Описание Цикл INSERT/UPSERT в `osm_poi_ekb` выполняет `db.execute(...)` напрямую, без `with db.begin_nested():` per-row, в отличие от соседнего `noise_loader.sync_noise_sources_to_db` (там SAVEPOINT есть, `noise_loader.py:247-285`). На psycopg3/SQLAlchemy любой сбой одного INSERT (битый tag-jsonb, нарушение constraint, кодировка, PostGIS-ошибка у way/relation без корректного center) переводит транзакцию в aborted-состояние; все последующие `db.execute` в цикле падают с `current transaction is aborted`, управление улетает в outer `except` → `db.rollback()` откатывает ВСЮ загрузку. Один дефектный OSM-элемент обнуляет весь еженедельный POI-sync. ## Корень Нарушено документированное правило проекта `.claude/rules/backend.md` (SAVEPOINT pattern) и vault `Bug_Pzz_Loader_Missing_Savepoint`. Контрпример — соседний `noise_loader.py` — уже использует `with db.begin_nested():` с явным комментарием об этой же проблеме. ## Evidence `poi_loader.py:124-191` — цикл `for el in elements:` с голым execute (строки ~158-186): ```python result = db.execute( text("""INSERT INTO osm_poi_ekb ... ON CONFLICT (osm_type, osm_id, category) DO UPDATE ... RETURNING (xmax = 0) AS is_insert"""), {...}, ).scalar() # ← нет 'with db.begin_nested():' вокруг ``` Внешний `except Exception` (~194) вызывает `db.rollback()`. ## Suggested fix Обернуть каждый INSERT в SAVEPOINT и считать сбой как skipped (по образцу `noise_loader.py:247-285`): ```python for el in elements: try: with db.begin_nested(): result = db.execute(text("INSERT ... RETURNING (xmax = 0) AS is_insert"), {...}).scalar() inserted += 1 if result else 0 updated += 0 if result else 1 except Exception as e: logger.warning("poi_sync insert failed for %s/%s: %s", osm_type, osm_id, e) skipped += 1 ``` Затем один `db.commit()` в конце. ## Обоснование severity high: потеря всего еженедельного POI-sync из-за одного дефектного OSM-элемента; путь достижим (weekly Celery beat, реальные OSM-данные). confidence 0.97. --- _week ревью 1 · multi-agent аудит 2026-06-14 · severity=**high** · category=`data-integrity`_ Затронутые файлы: - `/Users/anton/Птица/gendesign/backend/app/services/site_finder/poi_loader.py` - `/Users/anton/Птица/gendesign/backend/app/services/site_finder/noise_loader.py`
bot-backend added the
week ревью 1
label 2026-06-14 18:08:45 +00:00
bot-backend added the
scope/backend
label 2026-06-15 11:01:49 +00:00
Owner

🔍 week ревью 1 — верификация (ЧАСТИЧНО)

Текущее состояние кода: poi_loader.py:122-194 — цикл for el in elements: с голым db.execute(text("INSERT ... ON CONFLICT ... RETURNING (xmax = 0)"), {...}).scalar() на :156-184, БЕЗ with db.begin_nested():. Внешний except Exception на :192-194 делает db.rollback() + raise. Дефект подтверждён: одна aborted-транзакция (битый tags-jsonb, constraint, PostGIS-ошибка) → все последующие execute падают с current transaction is aborted → outer rollback откатывает весь weekly POI-sync.

ВАЖНО — контрпример из тела issue неверен: noise_loader.sync_noise_sources_to_db (noise_loader.py:157-235) на текущем main ТОЖЕ использует голый db.execute (:206) + outer except → db.rollback() (:233-234), БЕЗ with db.begin_nested():. Т.е. SAVEPOINT-паттерна, на который ссылается issue (noise_loader.py:247-285), в текущем коде нет — noise_loader подвержен той же проблеме. Канонический образец SAVEPOINT — .claude/rules/backend.md + domrf_kn.py.

Корень: Нарушено правило проекта .claude/rules/backend.md (SAVEPOINT per-row в loop upsert) — bare rollback в цикле откатывает всю outer tx. Касается обоих loader'ов.

Что править:

  • Обернуть каждый INSERT в with db.begin_nested():, считать сбой как skipped с logger.warning(... osm_type/osm_id, e), продолжать цикл; один db.commit() в конце (как в каноне backend.md).
  • Применить тот же фикс к noise_loader.sync_noise_sources_to_db (:206) — он подвержен идентичному дефекту.

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

  • Код: poi_loader.py:122-196 (loop + INSERT :156, outer except :192), noise_loader.py:157-235 (тот же паттерн), domrf_kn.py (рабочий SAVEPOINT-образец), .claude/rules/backend.md (SAVEPOINT pattern). Добавить тест: один битый элемент в батче не обнуляет остальные вставки.
  • Vault: Bug_Pzz_Loader_Missing_Savepoint_May14 (цитируется в backend.md как тот же класс бага).
**🔍 week ревью 1 — верификация (ЧАСТИЧНО)** **Текущее состояние кода:** `poi_loader.py:122-194` — цикл `for el in elements:` с голым `db.execute(text("INSERT ... ON CONFLICT ... RETURNING (xmax = 0)"), {...}).scalar()` на `:156-184`, БЕЗ `with db.begin_nested():`. Внешний `except Exception` на `:192-194` делает `db.rollback()` + `raise`. Дефект подтверждён: одна aborted-транзакция (битый tags-jsonb, constraint, PostGIS-ошибка) → все последующие execute падают с `current transaction is aborted` → outer rollback откатывает весь weekly POI-sync. **ВАЖНО — контрпример из тела issue неверен:** `noise_loader.sync_noise_sources_to_db` (`noise_loader.py:157-235`) на текущем main ТОЖЕ использует голый `db.execute` (`:206`) + outer `except → db.rollback()` (`:233-234`), БЕЗ `with db.begin_nested():`. Т.е. SAVEPOINT-паттерна, на который ссылается issue (noise_loader.py:247-285), в текущем коде нет — noise_loader подвержен той же проблеме. Канонический образец SAVEPOINT — `.claude/rules/backend.md` + `domrf_kn.py`. **Корень:** Нарушено правило проекта `.claude/rules/backend.md` (SAVEPOINT per-row в loop upsert) — bare rollback в цикле откатывает всю outer tx. Касается обоих loader'ов. **Что править:** - Обернуть каждый INSERT в `with db.begin_nested():`, считать сбой как `skipped` с `logger.warning(... osm_type/osm_id, e)`, продолжать цикл; один `db.commit()` в конце (как в каноне backend.md). - Применить тот же фикс к `noise_loader.sync_noise_sources_to_db` (`:206`) — он подвержен идентичному дефекту. **Что посмотреть:** - Код: `poi_loader.py:122-196` (loop + INSERT `:156`, outer except `:192`), `noise_loader.py:157-235` (тот же паттерн), `domrf_kn.py` (рабочий SAVEPOINT-образец), `.claude/rules/backend.md` (SAVEPOINT pattern). Добавить тест: один битый элемент в батче не обнуляет остальные вставки. - Vault: `Bug_Pzz_Loader_Missing_Savepoint_May14` (цитируется в backend.md как тот же класс бага).
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#1343
No description provided.