feat(tradein/scheduler): cooperative SIGTERM-drain — in-flight scrape unit commits before exit (phase 2) #2070
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#2070
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-scheduler-graceful-drain"
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?
Зачем
Фаза 2 развязки scraper-деплоя (после #2065 фаза 0+1). На деплое Docker шлёт scraper'у SIGTERM (
stop_grace_period: 120s). Раньшеscheduler_mainделал жёсткийtask.cancel()→ карточка гибла в полёте. Теперь SIGTERM кооперативный: текущая единица работы доделывается и коммитится, процесс выходит чисто в рамках grace.Механизм
app/core/shutdown.py(новый, leaf-модуль, без circular import):request_shutdown()/shutdown_requested()поверх module-levelasyncio.Event.scheduler_main.py: SIGTERM/SIGINT →request_shutdown()(вместо cancel)._await_schedulerгонит координатор противasyncio.wait_for(task, 100s); не уложился → hard-cancel fallback (нормальный crash по-прежнему всплывает черезtask.result()).scheduler.pyscheduler_loop: наshutdown_requested()— не клеймит новое, выходит из tick-loop.is_cancelled—avito_detail_backfill(между карточками → break →mark_donepartial; snapshot = pending-query, само-резюмится),import_rosreestr_dkp(между батчами →update_heartbeat+mark_donepartial). Drain =mark_done(partial), неmark_cancelled(это user-cancel семантика, не тронута).Pre-push fix (нашёл code-review)
_await_schedulerдренил только координатор, а работа бежит в detachedcreate_taskдетях (26trigger_*сайтов) →asyncio.runteardown хард-кансельнул бы их (тот же #1182). Фикс: registry_inflight_tasks(helper_spawn_trackedзаменил все 26create_task-сайтов) +_drain_inflight()(одинasyncio.wait, бюджет 80с < 100с < 120с), вызывается изscheduler_loopна shutdown. (Выбран точный registry, а неasyncio.all_tasks()— последний включил бы родительский_run→ circular wait.)Честный scope-boundary
~10 других
is_cancelled-чек-пойнтов вscrape_pipeline.py(city-sweeps/full-loads) не заведены в кооперативный drain — они коммитят инкрементально (теряют ≤1 карточку) и упираются в safety-net hard-cancel. Это приемлемая граница фазы 2; полное покрытие — follow-up.Безвредность
shutdown_requested()== False → поведение идентично текущему.is_cancelled/reap_zombiesсемантика не тронута. Дренированный run =done(reaper не пометит зомби).Тесты
tests/core/test_shutdown.py,tests/test_scheduler_main.py(drain calls request_shutdown не cancel; wait_for-timeout → hard-cancel),tests/test_scheduler.py(load-bearing: detached кооперативный ребёнок дренится, НЕ кансельнут; non-coop → timeout; no-op; loop→drain wiring),tests/tasks/test_avito_detail_backfill.py(shutdown → break + mark_done partial). Локально: ruff-100 clean,pytestцелевой 59 passed, regression sweep 254 passed.Refs #1182. На мерж — после ОК (правка критичного планировщика).