Сбор МЕРА: упавший прогон не висит zombie 6 часов, провалившаяся фаза не попадает в чекпоинт, проверка отмены не держит транзакцию часами #3561
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#3561
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/kit-orchestration"
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?
Группа задач по оркестрации scraper-kit: планировщик, чекпоинты свипов, транзакции. Семь коммитов (четыре исходных и три по ревью) и merge
origin/main. Миграций нет.Правки по ревью (17.09)
Исправлено
test_domclick_phase_timeout_keeps_completed_buckets_and_is_not_doneтребовалdone_buckets == ['st']после таймаута фазы. Лоты ДомКлика пишутся однимsave_listingsпосле всех корзин, поэтому снятая watchdog'ом фаза не сохраняет ничего, но чекпоинт получалcompleted_bucketsживого скрейпера. Исправил код, а не только тест: чекпоинт пополняется, только если фаза дошла до конца, то есть послеsave_listings(флаг_saved). Тест развёрнут:test_domclick_unsaved_buckets_are_not_checkpointed[fetch_timeout|save_failed]проверяетdone_buckets == []и статусfailed. Контроль «полный проход несёт унаследованное ∪ пройденное» по-прежнему даёт test_3369. Сами лоты при таймауте теряются как и раньше: для этого нужно инкрементальное сохранение (п.1 миграции 308), в этот PR оно не входит.scheduler._dispatchвruns.mark_crashed(db, run_id, exc). Её зовут планировщик и все пять ручек:avito-city-sweep,cian-city-sweep,cian-full-load,yandex-full-load,yandex-city-sweep. Заявление «общая точка для всех хендлеров» из первой версии было верно только для планировщика. Корень (входBrowserFetcherдоtryфинализатора пайплайна) не трогал: он в каждом пайплайне свой, а внешняя финализация покрывает все пути.mark_failedиraise, после чего планировщик звалmark_failedещё раз. UPDATE ничего не менял, но_alert_on_run_idсрабатывал с тем же стриком: на вехе лестницы в Sentry уходил дубль и писался WARNING «no-op». Теперьmark_crashedсначала читает статус (послеrollback, транзакцию не держит) и финализирует толькоrunning.failed >= 1; единственный отказавший дом якоря ловит порог>= 2; упавший запрос домов ловит снятыйhouses_attempted += len(nb_id_list).Closesзаменены наRefs. Приёмка у обоих на проде после деплоя.Проверено и уточнено (не опровергнуто)
yandex_city_sweep5289 (29.08) иcian_history_backfill6173. Все 13 сняты через 32 с – 2 мин после последнего пульса, пары 5289/5290, 5610/5614 и 6167/6173 сняты в одну секунду. Это reap при пересоздании контейнера (#3391), а не 6-часовой reaper. Zombie с пульсом, умершим больше чем за час до снятия, за 21 сутки 0. Это обоснование гипотезы «зависания в данных нет» по данным, а не по построению запроса. Все 18 мгновенных zombie сняты ровно через 6:00–6:01 после старта._anchor_timed_outв yandex-свипе (эквивалентная мутация) была и вorigin/main(стр. 2694/3000). В этом PR не трогал.tradein-scraper= 172.19.0.4,tradein-tgbot= 172.19.0.3 (docker inspect, контейнеры стартовали 08:36 UTC). На 172.19.0.3 виситidle in transactionтг-бота сSELECT value FROM tg_support_state, возраст 16 с в 08:58.countersнетdone_bucketsвообще (обаdoneсinterrupted=1), поэтому сброс по её п.4 у этих строк сбрасывать нечего.#1940 — упавший до финализатора прогон висел «running» 6 часов, потом zombie (частично)
Что было. За 21 сутки на проде 18 avito-свипов ушли в
zombie: 7×avito_newbuilding_sweep, 3×_serov, 3×_nizhniy_tagil, 2×avito_city_sweepи др. У всехheartbeat_at - started_at < 5 с, аfinished_atстоит ровно через 6 ч.Почему. Лог
tradein-scraperпо 7349 (17.09): через 10 мс после claimBrowserFetcher.__aenter__→_acquire_leaseбросаетNoProxyAvailableError(avito). Вrun_avito_city_sweepвызовstack.enter_async_context(BrowserFetcher(...))стоит доtryсmark_failed. Запускающий код (планировщик и ручки админки) исключение только логировал.Что сделано.
runs.mark_crashed: если строка ещёrunningи в цепочке причин есть пустой пул (caused_by_no_proxy), ставитсяbannedсban_kind=infra, иначеfailedсcrashed: <тип>: <текст>. Уже финализированную строку не трогает и повторноmark_*не зовёт. Вызывается изscheduler._dispatchи пяти ручных запусков вadmin.py. Причина пустого пула avito в правку не входит.Не сделано: п.2 issue, жёсткий таймаут на запуск браузера и вызов сайдкара. По данным за 21 сутки зависаний с умирающим пульсом нет (см. выше), но без таймаута такой случай снова уйдёт в zombie через 6 ч. Поэтому
Refs.#3415 — якорь попадал в done_buckets, даже если фаза в нём отказала полностью
Что было.
cian_city_sweep6179 (06.09) получилfailed(«houses 30 из 30»), но вdone_bucketsзаписаны все 5 якорей. Прогон 6276 резюмировался от него, пропустил все якоря и получилdoneсhouses_attempted=0.Почему. Отметку «якорь пройден» ставил только поток управления, а фаза отказывает и без исключения:
fetch_newbuildingвозвращает None, растёт только счётчик.Что сделано. Гейт
_bucket_phase_totally_failedсверяет прирост парX_attempted/X_failedза якорь, порог — одна попытка. Стоит в cian- и avito-свипах. В ветке «houses DB query failed»houses_attemptedрастёт вместе сhouses_failed. Форма чекпоинта прежняя.#3480 — транзакция в БД tradein жила дольше часа (частично)
Виновник по живому снимку.
pg_stat_activity17.09 07:48 UTC: pid 1933154, client 172.19.0.3 (тогда это былtradein-scraper, сверено черезdocker inspect),idle in transaction, возраст транзакции 1:35:41, последний запросSELECT status FROM scrape_runs WHERE id = $1(runs.is_cancelled). Прогонdomclick_city_sweep_moskva7344 стартовал в 06:12:48.598, транзакция — в 06:12:48.628.Совпадения по истории. Prometheus
pg_activity_horizon_oldest_xact_age_s{db="tradein"}, 03.09–17.09: 61 точка из 4127 больше 3600 с, 12 окон. 10 из них совпадают со свипами ДомКлика, 2 окна (07.09 04:05–04:30, 10.09 22:30–23:35) не атрибутированы.Что сделано. В
runs.is_cancelledпосле чтения добавленdb.commit(), общая точка для всех 12 вызовов.Не закрыто: остаются другие места «чтение, потом сеть» без commit. Например,
SELECT priority_rowsперед detail-циклом и чтения в якорях avito/cian. ПоэтомуRefs.#2406 — крайние случаи city-свипов kit'а
Проверки по значению в строке прогона (фейковая БД мержит counters, как jsonb-мерж):
errors_countрастёт;save_listings, не отмечает корзины:done_buckets == [], статусfailed(исправлено по ревью, см. выше);interrupted=1, в чекпоинте ровно первый якорь;mark_doneне вызывается.Таймаут и дрейн у avito уже покрывал test_3319, дрейн до первого якоря — test_3333, отмену у domclick — test_3369.
Не сделано: #3043 (geoCoords Авито)
В issue сказано: «решение по форме — за владельцем». Замена якорного цикла на citywide-проход меняет единицу чекпоинта (#3074/#3319/#3415) и логику подхвата, а в #3541 (16.09) только что завели сетку из 25 якорей по Москве. Без решения владельца не трогаю. Для решения: прогон 7283 (
avito_city_sweep_moskva) —lots_fetched=982,inserted+updated=237.Тесты
tradein-mvp/backend, весь сьют:6254 passed, 42 skipped, rc=0 (DATABASE_URL=… uv run python -m pytest tests/ -q -p no:cacheprovider, rc снят у pytest). До правок по ревью было 6244.test_1940_crashed_run_is_finalized.py(10: планировщик 5, ручки админки 5),test_3415_bucket_done_only_if_phases_ok.py(8),test_3480_no_open_transaction_over_http.py(2, настоящая SQLAlchemy-сессия на SQLite),test_2406_sweep_edge_cases.py(9).ruff check app tests ../packages/scraper-kit/srcчисто,ruff format --checkпо 8 изменённым файлам чисто.packages/scraper-kitсвоих тестов нет, тесты kit'а живут вbackend/tests.Фальсификация (фикс ломался руками, копия в scratchpad, потом восстановление и
diff -q)Первая версия:
3 failed—assert 'running' == 'banned',assert 'running' == 'failed'.3 failed, 2 passed—якорь с полностью отказавшей houses-фазой помечен пройденным — чекпоинт ['ekb-center', 'ekb-south'].db.commit():2 failed—assert True is False(in_transaction()).continueв таймауте cian,returnв таймауте yandex, меткаinterruptedдрейна,returnотмены у cian/yandex/avito): каждая роняет свой тест, напримерassert [56.83, 56.79] == [56.83].По ревью:
if _saved:заменён наif True::2 failed, 7 passed—AssertionError: корзины без единой сохранённой строки в чекпоинте: ['st'](оба параметра). То же при_saved = Trueв начале фазы.mark_crashed:1 failed, 9 passed—assert 2 == 1(capture_message.call_count), в логе дваждыsentry alert sent: source=avito_city_sweep has 3 consecutive failed/banned runsиmark_failed no-op.mark_crashedзаменён наpass:5 failed, 5 passed—assert ('running', None) == ('banned', 'infra').pass:3 failed, 7 passed. Ветка no-proxy заменена наif False:7 failed—assert ('failed', None) == ('banned', 'infra').attempted >= 2:2 failed—якорь с 1 из 1 отказавшим домом помечен пройденным: ['ekb-center', 'ekb-south'];failed >= 1:1 failed—якорь с частичным отказом houses не помечен пройденным: []; снятhouses_attempted += len(nb_id_list):1 failed—якорь с упавшим запросом домов помечен пройденным: ['ekb-center', 'ekb-south'].Деплой
Пересоздаются
tradein-backend,tradein-tgbotиtradein-scraper(один образ, в него вкомпилирован scraper-kit). Миграций нет. Перед мержем в том же блоке, что и мерж:SELECT id, source FROM scrape_runs WHERE status='running'. На 17.09 08:58 UTC там пусто: 7349/7350 сняты boot-reap какcancelledв 08:36. Окна: ЕКБdomclick_city_sweep03:00–06:00 UTC (следующий запуск 18.09 03:50), ночьюcian_full_load/cian_detail_backfill.Приёмка на проде (после деплоя)
SELECT count(*) FROM scrape_runs WHERE status='zombie' AND started_at > '<деплой>'— все zombie, по классам:heartbeat_at - started_at < 5 s(мгновенные) = 0;finished_at - heartbeat_at > 1 h(зависание, снятое 6-часовым reaper'ом) = 0. Если такие есть, это п.2 issue, который здесь не делался;finished_at - heartbeat_at < 5 min) относятся к #3391 и отдельны.crashed run_id=Nв логеtradein-scraperилиtradein-backend(ручные запуски) — это N со статусомbanned/failedв пределах секунды.failed-завершение свипа в Sentry не больше одного сообщенияconsecutive failed/banned runs.cian_city_sweep*с полностью отказавшей фазой не несёт этот якорь вcounters->'done_buckets'. Следующий прогон сresume_reason=okпоказываетhouses_attempted>0(илиdetail_attempted>0). Прогон, пропустивший фазу по резюму, приёмкой не считается.domclick_city_sweep_moskva/_moskovskaya_oblast(самые длинные окна, 7344 — 1.6 ч), поэтому метрика улучшится и без этой правки.domclick_city_sweep(первый — 18.09 с 03:50 UTC) сначала взять IP скрейпера:docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' tradein-scraper(на 17.09 это 172.19.0.4; 172.19.0.3 сейчас у тг-бота, у него своя короткаяidle in transaction).SELECT pid, client_addr, state, now()-xact_start, query FROM pg_stat_activity WHERE datname='tradein' AND state LIKE 'idle in transaction%' AND client_addr='<IP скрейпера>': нет сессии старше минуты приrunningсвипе.pg_activity_horizon_oldest_xact_age_s{db="tradein"} > 3600. Если окно повторится, снятьpg_stat_activityв момент тревоги: два окна вне ДомКлика (07.09, 10.09) не атрибутированы, а «чтение, потом сеть» остаётся в avito/cian.Closes #2406
Refs #1940 (п.2 issue — таймаут запуска браузера — не делался; приёмка до 25.09)
Refs #3415 (приёмка — первый cian_city_sweep с отказавшей фазой)
Refs #3480 (закрыть после недели без окон > 1 ч)
Refs #3043
🤖 Generated with Claude Code