fix(ptica): упавший прогон Объектива помечается failed, а не висит running вечно (#2464) #2972

Merged
bot-backend merged 1 commit from fix/2464-objective-run-stuck into main 2026-08-20 11:04:08 +00:00
Collaborator

Пункт эпика #2464: scrape_objective.py:252.

Дефект

except Exception as e:
    if run_id:
        try:
            _finish_run(db, run_id, status="failed", ...)
        except Exception:
            pass          # ← молча

Если исходный сбой был DB-level (напр. INSERT в _save_raw), транзакция остаётся в aborted-состоянии: _finish_run падает уже на своём execute, отказ гасится голым pass, и строка прогона навсегда остаётся в status='running'.

Замер прода 20.08

objective_scrape_runs:
   done     71   последний 2026-08-19 21:30
   running   6   последний 2026-05-17 16:45

зависшие: run_id 1..6, возраст 2274 часа — 95 суток

Уборщика зомби для objective_scrape_runs нет — в отличие от cadastre, где он есть (cadastre-zombie-cleanup в расписании). Поэтому строки и висят с мая.

Правка

rollback перед _finish_run. Сессия здесь своя (SessionLocal() в этой же функции, close в finally), поэтому плоский rollback законен: он отбрасывает уже провалившуюся транзакцию и ничего чужого не теряет.

Плюс отказ самого _finish_run больше не молчит: если и после rollback не прошло — это логируется. Знать об этом важнее, чем сохранить тишину в логе; именно тишина и держала шесть строк незамеченными 95 суток.

Тест

На PostgresLikeSession — двойнике с настоящей семантикой aborted-транзакции. На MagicMock был бы зелёным по построению.

Против origin/main:

UPDATE статуса не выполнился, журнал SQL пуст  → падает
сессия закрывается в finally      → контроль, зелёный с обеих сторон
исходная ошибка пробрасывается    → контроль, зелёный с обеих сторон

Второй контроль не для симметрии: ловит «починку», которая заодно погасила бы исключение — тогда Celery считал бы упавший прогон успешным.

Чего не делаю

Шесть уже висящих строк не трогаю. Правка предотвращает новые, а чистка старых — отдельное решение по данным прода. На них ничего не завязано: с 17.05 прошёл 71 успешный прогон.

Прогоны

tests/workers   226 passed   rc=0
Пункт эпика #2464: `scrape_objective.py:252`. ## Дефект ```python except Exception as e: if run_id: try: _finish_run(db, run_id, status="failed", ...) except Exception: pass # ← молча ``` Если исходный сбой был DB-level (напр. INSERT в `_save_raw`), транзакция остаётся в aborted-состоянии: `_finish_run` падает уже на своём `execute`, отказ гасится голым `pass`, и строка прогона **навсегда** остаётся в `status='running'`. ## Замер прода 20.08 ``` objective_scrape_runs: done 71 последний 2026-08-19 21:30 running 6 последний 2026-05-17 16:45 зависшие: run_id 1..6, возраст 2274 часа — 95 суток ``` Уборщика зомби для `objective_scrape_runs` нет — в отличие от cadastre, где он есть (`cadastre-zombie-cleanup` в расписании). Поэтому строки и висят с мая. ## Правка `rollback` перед `_finish_run`. Сессия здесь **своя** (`SessionLocal()` в этой же функции, `close` в `finally`), поэтому плоский rollback законен: он отбрасывает уже провалившуюся транзакцию и ничего чужого не теряет. Плюс отказ самого `_finish_run` больше не молчит: если и после rollback не прошло — это логируется. Знать об этом важнее, чем сохранить тишину в логе; именно тишина и держала шесть строк незамеченными 95 суток. ## Тест На `PostgresLikeSession` — двойнике с настоящей семантикой aborted-транзакции. На `MagicMock` был бы зелёным по построению. Против `origin/main`: ``` UPDATE статуса не выполнился, журнал SQL пуст → падает сессия закрывается в finally → контроль, зелёный с обеих сторон исходная ошибка пробрасывается → контроль, зелёный с обеих сторон ``` Второй контроль не для симметрии: ловит «починку», которая заодно погасила бы исключение — тогда Celery считал бы упавший прогон успешным. ## Чего не делаю Шесть уже висящих строк не трогаю. Правка предотвращает новые, а чистка старых — отдельное решение по данным прода. На них ничего не завязано: с 17.05 прошёл 71 успешный прогон. ## Прогоны ``` tests/workers 226 passed rc=0 ```
bot-backend added 1 commit 2026-08-20 10:42:34 +00:00
fix(ptica): упавший прогон Объектива помечается failed, а не висит running вечно (#2464)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m53s
CI / backend-tests (pull_request) Successful in 17m10s
7bc26ca260
Обработчик выглядел так:

    except Exception as e:
        if run_id:
            try:
                _finish_run(db, run_id, status="failed", ...)
            except Exception:
                pass          # ← молча

Если исходный сбой был DB-level (напр. INSERT в _save_raw), транзакция остаётся в
aborted-состоянии: _finish_run падает уже на своём execute, отказ гасится голым
pass, и строка прогона навсегда остаётся в status='running'.

Замер прода 20.08: шесть таких строк висят с 17.05 — 2274 часа, 95 суток. Уборщика
зомби для objective_scrape_runs нет (в отличие от cadastre, где он есть).

Правка: rollback перед _finish_run. Сессия здесь СВОЯ (SessionLocal() в этой же
функции, close в finally), поэтому плоский rollback законен — он отбрасывает уже
провалившуюся транзакцию и ничего чужого не теряет. Плюс отказ самого _finish_run
больше не молчит: если и после rollback не прошло, это логируется — знать об этом
важнее, чем сохранить тишину.

Тест на PostgresLikeSession — двойнике с настоящей семантикой aborted-транзакции.
На MagicMock он был бы зелёным по построению. Против origin/main:

  UPDATE статуса не выполнился, журнал SQL пуст  → падает
  сессия закрывается в finally      — контроль, зелёный с обеих сторон
  исходная ошибка пробрасывается    — контроль, зелёный с обеих сторон

Второй контроль не для симметрии: ловит «починку», которая заодно погасила бы
исключение — тогда Celery считал бы упавший прогон успешным.

Шесть уже висящих строк не трогаю: правка предотвращает новые, а чистка старых —
отдельное решение (данные прода, и на них ничего не завязано: с 17.05 прошёл 71
успешный прогон).

Прогоны: tests/workers — 226 passed rc=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit cfa0046b34 into main 2026-08-20 11:04:08 +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#2972
No description provided.