fix(scraper-kit/scheduler): SIGTERM-drain помечает in-flight прогоны interrupted=1 при hard-cancel; честный лог drain'а (#3391) #3392
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3392
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/drain-marks-inflight-app-tasks"
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?
Closes #3391.
Дефект (прод 07.09 02:36 UTC, первый реальный SIGTERM-drain после #3363). Деплой пересоздал scraper при идущих
cian_detail_backfill6167 (75 мин, 162 объявления) иcian_history_backfill6173; через 100 с grace — hard-cancel; строки осталисьrunning, boot-reap нового контейнера сделал ихzombieбезinterrupted.interrupted=1на SIGTERM писали только kit-пайплайны и DKP; app-task бэкфиллы — нет. Плюсscheduler_mainпечатал «scheduler drained cleanly (SIGTERM)» и после hard-cancel.Фикс в одном месте (kit-scheduler).
orchestration/scheduler.py:354-380— реестр_inflight_run_ids: dict[Task,int]черезspawn_tracked(coro, run_id=…)из_dispatch(:975; claim не тронут), done-callback чистит.mark_inflight_interrupted(tasks)(:380, синхронная — безawait, живёт внутриexcept CancelledError): для каждого run_idSELECT status, counters; еслиrunning→counters["interrupted"]=1поверх прочитанных иmark_done. Вызывается и по истечении grace (:461), и вexcept asyncio.CancelledError(:449, затемraise) — на проде hard-cancel прилетает внутрьasyncio.wait(хвост tick-сна до 60 с + 80 с дрейна > 100 с grace). Counters читаются из строки, потому что боевойctx.runs—app.services.scrape_runs, гдеmark_donecounters заменяет (#3390): голый{"interrupted":1}стёр быdone_buckets. «Не трогать успевших» — проверка статуса + собственный гейтmark_doneWHERE status='running'.app/scheduler_main.py:156,237-244—_await_schedulerвозвращает признак hard-cancel; «drained cleanly» печатается только когда задача вышла сама.stop_grace_period: 120 с у scraper против_DRAIN_TIMEOUT_S=100→ 20 с на запись (SELECT + UPDATE RETURNING на прогон, единицы мс) — по построению успевает; компоуз не менял.Тесты
tests/test_3391_drain_marks_inflight_app_tasks.py: настоящиеSchedulerContext/_dispatch/drain_inflight; застрявшая корутина (sleep(10)), соседняя финализируется сама; фейковыйmark_doneмоделирует app-копию (замена + гейтrunning) — голая метка стёрла бы чекпоинт. Плюс caplog-тест на отсутствие «drained cleanly» при hard-cancel.Фальсификация (
git apply -Rисходников, тесты на месте):assert 'running' == 'done'×2 и'drained cleanly' not in …— 3 failed, rc=1; после возврата — зелёное.Прогоны: полный backend
5521 passed, 35 skipped(rc=0); ruff OK.Приёмка на проде: следующий деплой при идущем detail-бэкфилле — в логах scraper
scheduler: drain — N прогон(ов) сняты с 'running' как interrupted: […]и нет «drained cleanly» после «hard-cancelling»;scrape_runs:status='done'(илиfailedпо honest-status-гейту при доле отказов ≥15 %),interrupted='1',boot_reapedпуст, прежние счётчики на месте; новый контейнер:boot-reap — … нет (0).Deep-ревью (07.09): approve с предупреждениями. Подтверждено по коду: пометка синхронная, своя сессия (
RealSessionFactory), между SELECT иmark_doneнет точек передачи управления; гейтWHERE status='running'есть в обеих копияхmark_done/mark_failed; резюм дляfailed+interruptedработает ('failed'в_RESUME_STATUSES); после hard-cancel новыхawaitнет, 20 с до SIGKILL хватает; обе мутации краснеют ('running' == 'done',KeyError: 'done_buckets').Докатываю до мержа: (1)
update_heartbeatв обеих копиях без гейта по статусу — живая задача следующим пульсом стираетinterrupted(app-копия заменяет counters) →AND status='running'; (2) hard-cancel в теле тика (не внутриdrain_inflight) метку не ставит —except CancelledErrorвокруг тик-лупа; (3)db.rollback()вexcept— иначе первый отказ SQL утащит остальные run_id; (4)session_factory()/close()внутриtry, чтобы не подменитьCancelledError; (5) честный список помеченных в WARNING + потолок «БД недоступна → SIGKILL через 20 с» в докстринге.Отдельно — #3393: оборванные деплоем прогоны теперь попадают в лестницы стриков (failed-ratio по частичным counters;
doneобнуляет стрик банов источника) — до #3392 такие строки былиzombieи в выборки не попадали.