fix(tradein/scraper): пропуск расписания пишет строку прогона со статусом skipped (#2658) #2662
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#2662
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/2658-loud-skip-status"
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?
Что было
Пропуск наступившего окна был немым:
logger+ сдвигnext_run_at, ни строки вscrape_runs, ни измененияlast_run_at.cian_history_backfillтак простоял 37 дней на протухших куках Циана (истекли 30.06 08:21 UTC,last_run_at= 29.06) и снаружи выглядел работающим — расписание исправно «переезжало» вперёд, а docker-логи сwarningтерялись на каждом редеплое.Статус
'skipped'заведён ещё миграцией 015 и локализован во фронте как «пропущено» — в проде у него было 0 строк (проверено:done|2926 banned|130 zombie|85 cancelled|50 failed|41,skippedотсутствует). Полностью построенный и ни разу не использованный механизм.Плюс алерт про куки стоял в недостижимой ветке — той, где
verify_sessionвернулNone. На протухших куках до неё не доходит никогда:load_sessionсам фильтруетexpires_at_estimate > NOW()и отдаётNoneещё в первой, немой ветке.Что стало
Пять мест, где расписание пропускалось без следа, теперь пишут строку
scrape_runs(status='skipped')с машиночитаемой причиной вerrorи человеческим пояснением вcounters.detail:_claim_run— уже есть running-прогонalready_running_claim_run— advisory-lock занят конкурентным тикомconcurrent_claim_claim_run— running появился под локом (double-check)running_appeared_under_lockscheduler_loop— enabled-расписание без handler'аunknown_sourcepre_claim(единственный в кодовой базе)cian_cookies_missing/cian_cookies_expired/cian_cookies_invalidЧисло совпало с заявленным в issue: 1
pre_claim+ 4 в планировщике kit. В пятом месте — две «немых» точки возврата (нет валидных кук / Циан не принимает куки), обе теперь пишут строку; слагов там три, чтобы «кук нет вовсе» отличалось от «протухли» и от «помечены невалидными».Схлопывание. Подряд идущие одинаковые пропуски одного source не плодят строк: обновляется последняя
skipped-строка (finished_at+ счётчикcounters.skips). Без этогоalready_runningиunknown_sourceписали бы строку каждый тик (60 с) — они не двигаютnext_run_at, и расписание переотбираетсяget_due_schedulesдо устранения причины. Побочный эффект приятный: уcian_history_backfillвместо 37 одинаковых строк была бы одна сskips=37,started_at30.06 и свежимfinished_at— «пропуски идут с такого-то, столько-то раз».Алерт. Обе ветки cookie-гейта теперь зовут
logger.error, а неsentry_sdk.capture_message(level="warning"):LoggingIntegration(event_level=ERROR)(scheduler_main.py:59) — ERROR-запись сама становится событием, а warning-уровень до него не дотягивает;try/except: passвокруг sentry-вызова и лишний импорт;scrape_runs, которая переживает редеплой.Предупреждаем заранее, а не по факту (
COOKIE_EXPIRY_WARN_DAYS = 5). Обновление кук — ручная операция (залить дамп через админку), человеку нужен запас: алерт по факту протухания приходит, когда сбор уже встал. Новый планировщик для этого не нужен — проверка встроена в тот же_cian_pre_claim, который и так исполняется раз в сутки в окне расписания;save_sessionставит TTL 30 дней, так что окно предупреждения широкое. Алерт по факту протухания при этом остался — заранее ≠ вместо.Монитор нулевых прогонов (#2625)
Не сломан и не смешан. Обе alert-выборки (
_alert_if_consecutive_failures,_alert_if_consecutive_zero_results) отбираютstatus IN ('failed','banned','done','cancelled')—'skipped'туда не попадает, поэтому пропуск не считается нулевым прогоном и не прерывает стрик реальных нулевых. Разводить было нечего: разделение уже обеспечено SQL-фильтром, тест это фиксирует (test_zero_result_monitor_ignores_skipped_rows).Честная оговорка: строки-пропуски существующему монитору «видимы» лишь в смысле «теперь их видно в БД и в UI» — в его счётчик они намеренно не входят. Громкость пропуска обеспечивает алерт из пункта выше, а не zero-result-монитор.
Тесты
Новый
tests/test_scrape_skip_visibility.py(16 тестов): запись строки в каждом из пяти мест, схлопывание, порядокrollback→mark_skipped(иначе INSERT улетел бы в откат advisory-лока), happy-path без skip-строк, достижимость ERROR-алерта на протухших куках, предупреждение заранее, тишина на свежих куках, неломание zero-result-монитора. Три существующих parity-теста_claim_runдополнены проверкойdb.skip_rows == 1.Фальсификация. С застэшенной реализацией новый файл падает целиком на
ImportError(новые символы) — это слабое «красное», поэтому отдельно прогнан пробник тем же сценарием, но без импорта новых имён: на старом коде он падает по ассерту, а в логе видно ровно немую ветку из issue:После возврата реализации — зелено. Полный прогон бэкенда:
3016 passed, 1 падение —tests/test_search_api.py::test_search_cache_hit(401 вместо 200), не связано с этим PR: оно падает и файлом в одиночку, а изменённые модули (scheduler/cian_session/product_handlers/kit runs) в search API не участвуют.Что НЕ входит
'skipped'уже в CHECK-констрейнте (015 + 051), колонкиerror/countersесть.mark_skippedдобавлен только в kit-копиюruns(строки-пропуски создаёт исключительно планировщик); расхождение с app-копией отмечено в её докстринге.Refs #2658
Правки по ревью — коммит
b800760c, CI зелёный.1. Фильтр
skipped(главное).Literalвadmin.py+RUN_STATUS_ALLвRunsTable.tsx.translateStatus(«пропущено») и нейтральный бейдж уже умели, так что чип появился без правок рендера. Тестtest_admin_runs_filter_accepts_skipped_statusчитает аннотацию эндпоинта — падает, если слаг выпадет изLiteral.2.
started_atпри схлопывании — выбран bump, не смена сортировки.started_at = NOW(), а начало стрика переезжает вcounters.first_skip_at(берётся из старогоstarted_at, поэтому точное, а не выводимое изskips× каденс).Почему не
ORDER BY GREATEST(started_at, finished_at): этотORDER BY— вlist_all/list_recent, то есть в списке по ВСЕМ источникам. Он молча переставил бы все строки (долгий прогон, закончившийся позже, прыгал бы выше начатого позднее), а чинить надо было ровно одну строку — schлопнутую. ПлюсGREATESTне ложится на(source, started_at DESC), то есть цена платится на каждом открытии таблицы, а не раз в тик. Bump локален, обратим и не меняет смысл «списка по времени запуска».3.
counters.detailосвежается. Заодно заменил три вложенныхjsonb_setнаCOALESCE(counters,'{}') || jsonb_build_object(...): короче, а главное —jsonb_set(target, path, NULL)обнуляет ВЕСЬcounters, и сdetail=Noneэто была бы мина.4. Индекс.
ORDER BY started_at DESC, id DESC— ложится наscrape_runs_source_started_idx (source, started_at DESC)из миграции 015;id DESCтолько разводит ties. Дляunknown_source(тик каждые 60 с бессрочно) это снимает пересортировку всех строк источника раз в минуту.5. Свой
try/exceptвокруг записи — сознательно НЕ добавлен. Еслиdb.executeпадает, то падает иhas_running_run/create_runследующего расписания в этом же тике: тик срывается в любом случае, «раньше не могло уронить» верно только для самой ветки, не для тика. А проглотить исключение здесь — вернуть ровно тот немой пропуск, ради которого заведён #2658 (лог есть, следа нет). Самовосстановление через 60 с,next_run_atне двигался, значит расписание останется due.6.
session_expires_at(valid_only=...)— забрал, правка дешёвая. Предупреждение «скоро протухнут» считает срок ИМЕННО той записи, которую взялload_session(фильтр валидности +last_invalid_at); диагностика послеNoneпо-прежнему смотрит на свежайшую любую — валидных там нет по определению. Один запрос, ветвление параметром, без f-string в SQL.Тесты. 20 в
test_scrape_skip_visibility.py(+4 новых), целевые сюиты85 passed, полный бэкенд зелёный на CI. Фальсификация безgit stash(патч-файл →checkout --→ прогон →git apply): на коде первого коммита падают по ассерту ровно 4 новых теста —collapse_refreshes_detail_and_started_at,latest_lookup_uses_indexed_order,warns_before_expiry_not_after(проверкаvalid_only=True),admin_runs_filter_accepts_skipped_status; послеgit applyснова зелено.