89 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 75b0931fad |
Merge pull request 'fix(cian): капча видна в отчёте прогона (ban_kinds.platform по типу) и банит узел на curl-пути (#3402 follow-up)' (#3406) from fix/3402-followups-ban-kinds-curl into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 4m10s
Deploy Trade-In / build-backend (push) Successful in 1m41s
Deploy Trade-In / deploy (push) Successful in 1m50s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
|
|||
| 224f91e8fa |
fix(cian): капча видна в отчёте прогона и банит узел на curl-пути (#3402)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m11s
Два follow-up из ревью #3403. 1. Капча-волна была невидима в счётчиках. `_note_refusal` ключевался только по HTTP-статусу, а капча приходит с 200 (свой детект по <title>) или без статуса (сайдкар) → рос один `listings_failed_fetch`, `ban_kinds` оставался пустым, и волна отказа площадки читалась как дрейф нашей разметки. Теперь диагноз берётся сперва по ТИПУ исключения (`ban_kind_of_exception`), статус — фолбэк. Инвариант #3196 сохранён: 'unknown' по типу И None по статусу по-прежнему ничего не пишут. `ban_kind_of_exception` расширен с `AvitoBlockedError` до `ProxyBanError` — это ровно тот mixin, по которому generic-прокси-слой уже снимает узел с выдачи источнику. Для Авито поведение не меняется (AvitoBlockedError его наследует), Cian/DomClick перестают приезжать как 'unknown'. 2. curl-путь детектил капчу, но не банил: общий parse-путь лежит ЗА границей `with curl_proxy_url(...)`, и `CianBlockedError` поднимался уже после `mark_health(ok=True)` — узел, которому Циан показывает капчу, оставался в выдаче Циану (дефект #2700, только на HTTP 200). Проверка перенесена ВНУТРЬ блока, `finally` хелпера сам делает `mark_banned(source='cian')`. Тесты по значению (оба красные на main): батч с капчей → ban_kinds == {platform: 1}; curl-путь + HTML капчи → mark_banned == [(1, 'cian')], mark_health(ok=True) нет. |
|||
|
|
b89788ee99 |
feat(tradein/proxy): прогон знает свой узел, а снятый бан перестаёт стирать историю (#3404)
Выбор оператора мобильного прокси опирался на две ненадёжные опоры. Первая: `scrape_runs` не знала, через какой узел шёл прогон — колонка `proxy_id` была только у банов и ротаций. «Какой узел собрал 5 карточек из 21» не выяснялось ни одним запросом. Вторая: `clear_source_bans` делала DELETE, а зовётся она после КАЖДОЙ успешной ротации exit-IP. У #540723 (МегаФон) 23 успешные ротации и ноль строк банов, у #540722 (Tele2) ротаций почти не было и 7 банов. «7 против 0» читалось как «Tele2 хуже», хотя в той же мере это «у МегаФона историю стёрли 23 раза». Теперь: - `scrape_runs.proxy_id` — последний выданный прогону узел; полная цепочка (если узел менялся mid-run) копится в `counters.proxy_ids`. Пишет `proxy_pool.attribute_run_proxy` из единственной точки — сразу после выдачи лиза в `acquire()`, поэтому curl-путь, браузерный sticky lease и ре-acquire при ротации покрыты одинаково. `run_id` доходит до адаптера через ContextVar (`scraper_kit.orchestration.run_context`): протокол `ProxyProvider.acquire` его не несёт, а `RealProxyProvider` живёт одним объектом на весь планировщик. Best-effort: `lock_timeout` 2с и проглоченное исключение — диагностика не вправе ронять выдачу прокси или ждать на блокировке строки прогона. - `clear_source_bans` гасит строку (`banned_until = now()`, `ban_count = 0`, `cleared_at`/`cleared_reason`) вместо удаления. Эскалация сохраняется 1:1: формула в `mark_banned` берёт ПРЕДЫДУЩИЙ `ban_count` показателем степени, при нуле это ровно `SOURCE_BAN_BASE_HOURS` — как после DELETE. Строка доживает до штатного purge по `SOURCE_BAN_PURGE_DAYS`. Для всех читателей `scrape_proxy_source_bans` погашенная строка неотличима от отсутствующей: acquire, оба guard-подзапроса `mark_banned`, `proxy_egress` (ранжирование по `ban_count` даёт 0, как у узла без истории), admin `_active_ban` — все гейтятся по `banned_until > now()`. Ничего не бэкфиллится: связать прошедшие прогоны с узлами нечем (`leased_by` исторически = NON_RUN_LEASE_MARKER), врать восстановленным значением нельзя. Миграция 287. Тесты: 9 новых на обе части (главный — эскалация после гашения даёт базовые 6ч, а не удвоенные) + 14 существующих переведены с DELETE-семантики на гашение, включая проверку, что секрет ротации не утекает в новое `cleared_reason`. Полный прогон бэкенда: 5600 passed, 37 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011WHFxVPWoBnSZihkdH1Uou |
||
| 36f2429fbe |
refactor(tradein/runs): одна реализация scrape_runs — kit, семантика counters мерж (#3390)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m58s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
Две живые копии одного модуля с противоположной семантикой counters: app
`mark_done`/`mark_failed`/`mark_banned`/`update_heartbeat` ЗАМЕНЯЛИ
(`counters = CAST(:counters AS jsonb)`), kit — МЕРЖИЛИ
(`COALESCE(counters,'{}') || …`). Расхождение дважды за сутки дало ложные
выводы на ревью (#3388 «отдать только флаг, остальное домержится» — на
replace это стёрло бы измеренное; #3355). Разошлись и другие места: гейт
статуса, `honors_cancel` у mark_cancelled (был только в app), `mark_skipped`
(только в kit), `mark_backfill_finished`/`distinct_sources` (только в app).
Реализация теперь одна — `scraper_kit.orchestration.runs`; в неё перенесены
app-only функции. `app.services.scrape_runs` — алиас kit-модуля через
sys.modules, а не реэкспорт имён: реэкспорт разводит патч-цели
(`patch("app.services.scrape_runs.sentry_sdk")`, `patch.object(runs_mod,
"mark_done")` правили бы глобаль модуля-обёртки, а тело функции читает
глобаль kit'а) — тест остался бы зелёным, не подменив ничего. С алиасом оба
имени ведут в единственную реализацию, и ни один из ~40 вызывающих и ~30
патч-сайтов в тестах не правится.
Победила семантика мержа: у строки прогона несколько писателей (пульс,
финализатор, дрейн), каждый знает лишь свои ключи, и замена теряла чужие —
чекпоинт done_buckets (#930), метку interrupted (#3391), замер из пульса
(#3384). Обратной зависимости («вызывающий рассчитывает, что финализатор
УДАЛИТ ключ заменой») нет: строка создаётся пустой в create_run, резюм читает
counters ПРЕДЫДУЩЕГО прогона по его id.
Тесты по значению на обоих путях импорта (двойник сессии читает SQL: `||`
против CAST, WHERE-гейт из текста): пульс {a:5} + mark_failed {b:1} → {a,b};
пульс/финализатор по финализированной строке — no-op; mark_cancelled
отказывает источнику, который отмену не опрашивает. На main эти тесты
красные для app-пути.
Комментарии в app/services/scheduler.py и kit/pipeline.py, утверждавшие про
живого «перезаписывающего двойника», приведены в соответствие.
|
|||
| e1f86e3c1b |
Merge pull request 'fix(scraper-kit/cian): фаза houses cian_city_sweep через пул прокси + стоп на пустом пуле (#3394)' (#3396) from fix/3394-cian-sweep-houses-proxy-pool into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 4m6s
Deploy Trade-In / build-backend (push) Successful in 1m48s
Deploy Trade-In / deploy (push) Successful in 3m1s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
|
|||
| fdae823762 |
fix(#3394): стоп по пустому пулу — по цепочке причин на уровне якоря; один errors_count; ban_kind=infra под тестом
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m55s
|
|||
| ba4fb03a05 |
fix(#3393): capped по сканированному окну — interrupted-строка не глушит лестницу навсегда; контроль zero-сторожа
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m55s
|
|||
| e12ece8fe6 |
fix(tradein/cian): houses-фаза cian_city_sweep идёт через пул прокси + стоп на пустом пуле (#3394)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m59s
`run_cian_city_sweep` звал `fetch_newbuilding(zhk_url, config=config)` без `proxy_provider`, хотя провайдер лежит в аргументах самого свипа и соседние фазы (SERP через CianScraper, detail через cian_fetch_detail) его передают. Внутри это давало `build_browser_fetcher(config, "cian", proxy_provider=None)` → `use_pool` эффективно False → POST /fetch без "proxy" → сайдкар брал свой env-узел SCRAPER_PROXY_URL, на проде выключенный (407 → camoufox InvalidIP → /fetch 503, факт #3386). Фаза houses давала 0/30 с 02.09 (run 6179: 24 × `houses failed ... 503 Service Unavailable`), строк `override=True` в логах сайдкара по ней не было ни одной. Остальные вызывающие `fetch_newbuilding` / `resolve_cian_zhk_url_via_search` провайдер уже передают (#2767/#2830/#3382), этот вызов был последним мимо пула. Второе: пустой пул поднимается ДО запроса, следующий дом упрётся ровно в то же самое — общий `except Exception` на дом превращал это в 30 одинаковых houses_failed и прогон уходил в 'done'. Теперь NoProxyAvailableError рвёт фазу и свип: `no_proxy_stop=1` в counters, mark_banned с ban_kind='infra' и сохранённым done_buckets (образец — #3389 yandex-nb-sweep, #3382). Ветка стоит ДО generic-except, иначе наш отказ инфраструктуры читался бы как «IP likely blocked» — бан площадки. |
|||
| 5f94bf0eeb |
fix(tradein/scrape_runs): оборванный деплоем прогон вне лестниц стриков и honest-status (#3393)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m57s
После #3392 SIGTERM-дрейн финализирует in-flight прогоны штатными mark_done/mark_failed с counters.interrupted=1 — раньше они оставались 'running' → 'zombie' и сторожей не касались. В популяции стриков эти строки судят частичные счётчики: - detail-бэкфилл, убитый на 20% отказов, получал 'failed' с диагнозом «сбор деградировал» (_failed_ratio_too_high) — диагноз про площадку, которого никто не измерял, — и входил в стрик неудач; - cian-бэкфилл (без attempted) получал 'done' и ОБНУЛЯЛ стрик банов — ровно вред, задокументированный в orchestration/scheduler.py:99 («5 банов подряд обнулил один 'cancelled' 09.08»). Прогон с меткой interrupted теперь считается так, будто его не было: он стрик ни продлевает, ни обнуляет (обе лестницы, обе копии модуля), а три honest-status-гейта в mark_done пропускаются — статус остаётся 'done' с меткой interrupted (конвенция #3319/#3333/#3355), причина в логе. 'cancelled' оставлен как был: там прогон прервал человек. SELECT сторожа неудач дополнен колонкой counters — по ней и идёт отбор. |
|||
| a7362bc5fa |
fix(#3391): пульс не пишет по финализированной строке; отмена в теле тика тоже помечает; rollback/try/честный лог
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m56s
Пять замечаний deep-ревью к PR #3392, ровно они.
1. Пульс стирал метку дрейна. `update_heartbeat` обеих копий бил `WHERE id = :run_id`
без гейта по статусу, а app-копия counters ЗАМЕНЯЕТ (#3390): задача, помеченная
`interrupted`, но ещё живая (ветка таймаута drain_inflight отдаёт её внешнему
hard-cancel'у — несколько итераций спустя, пульс на каждый батч —
app/services/scheduler.py:141), следующим же ударом стирала метку, и оборванный
прогон снова читался как полный проход. Гейт — `IN ('running', 'cancelled')`, а не
`= 'running'`: 'cancelled' финализирует строку, но задача встаёт лишь на ближайшей
границе якоря, и её последний пульс — ЕДИНСТВЕННЫЙ писатель чекпоинта в этот момент
(pipeline.py:1308/2368/2986/4488, mark_done там уже no-op по своему гейту), а
'cancelled' входит в _RESUME_STATUSES — сужение до 'running' молча съело бы точку
возобновления у каждой отмены. Возвращаемое значение update_heartbeat не читает
никто (обе копии -> None, ни одного присваивания на 130 сайтах вызова), так что
«0 строк обновлено» ломать нечего; no-op логируется WARNING'ом, как у mark_done.
2. Отмена вне drain_inflight. Hard-cancel приходит по расписанию grace'а
scheduler_main, а не по нашему, и может застать ТЕЛО тика (reap / stale-digest /
`_dispatch` с сетевым pre_claim). `except Exception` тика CancelledError не ловит,
до `await ctx.drain_inflight()` дело не доходит — строки оставались 'running'.
Тело вынесено в `_tick_loop`, `scheduler_loop` ловит CancelledError, помечает
in-flight и пробрасывает отмену.
3. rollback в except пометки: отказавший statement оставляет сессию в aborted-tx, и
первый же непроходимый run_id утаскивал все следующие (образец — defensive rollback
в mark_failed/mark_banned).
4. session_factory()/db.close() втянуты в try: исключение оттуда ЗАМЕНИЛО бы собой
CancelledError, а suppress(CancelledError) в scheduler_main его не глушит — процесс
уходил бы с трейсбеком вместо чистого drain-выхода.
5. WARNING перечисляет marked_ids, а не весь run_ids (там были и пропущенные по
статусу). В докстринге назван потолок: SELECT синхронный, у движка нет ни connect-,
ни statement-таймаута (app/core/db.py:8-19) — недоступная БД блокирует луп до
SIGKILL'а через 20 с docker-grace; данные при этом не хуже прежних (строки остаются
'running' → boot-reap).
Тесты — по значению, не по факту вызова; на исходниках
|
|||
| 30e3bacc5e |
fix(tradein/scheduler): SIGTERM-drain снимает с 'running' in-flight app-task'и (#3391)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m55s
Прод 07.09 02:36 UTC, первый настоящий drain после #3363: hard-cancel из scheduler_main оборвал дрейн, и два бэкфилла (cian_detail_backfill 6167, cian_history_backfill 6173) остались в scrape_runs со статусом 'running' — boot-reap следующего контейнера сделал их 'zombie' (boot_reaped=true), метки interrupted не было. interrupted=1 при дрейне писали только kit-пайплайны и DKP-импорт: у задач, чьё тело живёт в app, ставить её было некому. Метка ставится в единственной точке, через которую проходит любая detached run-задача — SchedulerContext.drain_inflight: и по истечении _CHILD_DRAIN_TIMEOUT_S, и в обработчике CancelledError (тот самый прод-путь). run_id берётся из нового реестра {task: run_id}, который заполняет _dispatch сразу после claim'а; claim-логика не тронута. Статус строки перечитывается перед записью, поэтому успевший финализироваться сам прогон не перезаписывается, а counters читаются из строки и дописываются — app-копия mark_done их ЗАМЕНЯЕТ (#3390), голая {"interrupted": 1} стёрла бы чекпоинт. scheduler_main: _await_scheduler возвращает признак hard-cancel'а, и строка «scheduler drained cleanly (SIGTERM)» больше не печатается сразу за WARNING'ом о превышении grace — на проде эти две строки стояли подряд и противоречили друг другу. Запас времени на запись: docker stop_grace_period 120s − _DRAIN_TIMEOUT_S 100s = 20 с после hard-cancel'а, запись синхронная (несколько statement'ов). |
|||
| f129d52cbe |
Merge pull request 'fix(scraper-kit): yandex _leaf под CAP не пишется в done-леджер как complete — capped_buckets отдельным счётчиком' (#3373) from fix/3368-yandex-leaf-complete-flag into main
Some checks failed
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m59s
Deploy Trade-In / deploy (push) Successful in 2m9s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m39s
Deploy Trade-In / perimeter-smoke (push) Failing after 11s
|
|||
| 9519c89d92 |
fix(scraper-kit/yandex): выпавшая страница leaf'а делает бакет неполным; capped_buckets переживает дрейн
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m5s
Ревью #3373, два minor: 1. `_leaf` считал полноту только по потолку страниц — упавшая страница (`payload=None` → `[]`) молча уходила в чекпоинт как собранная. Теперь паритет с cian по-настоящему: `complete = not capped and dropped_pages == 0`. 2. `counters.capped_buckets` присваивался ПОСЛЕ await — cancel/shutdown (RuntimeError из `_on_bucket`) уносил управление мимо строки. Перенесено в `finally`, счётчик виден в финальном payload дрейна. Нит: `ceil(total / 20)` → `_GATE_PAGE_SIZE`. Тесты по значению: бакет с 1 выпавшей страницей из 3 — не в done-леджере (+ контроль: 3 успешных страницы по-прежнему complete); дрейн после обрезанного бакета → `capped_buckets == 1` в финальных counters. Фейк `_DrainAtFirstBucket` (#3355) теперь объявляет `capped_buckets` явно: его catch-all `__getattr__` отдавал корутину на любое имя и ронял сериализацию counters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
|||
| 140caa4ddf |
fix(domclick): честная мотивировка чекпоинта + один _payload() на все ветки
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m3s
Правка ревью к PR #3372: комментарии и текст коммита обещали восстановление потери, которой нет. Факт: pipeline.py импортирует scraper_kit.orchestration.runs, и все четыре его писателя МЕРЖАТ jsonb (`counters = COALESCE(counters,'{}') || CAST(:counters AS jsonb)`, runs.py:708/776/817/880), а _pick_resume наследует done_buckets при claim (scheduler.py:741-748, #3074) — чекпоинт domclick в БД не терялся. Перезаписывающий двойник app/services/scrape_runs.py этой функцией не вызывается. Поэтому done_buckets в каждом payload — единообразие и защита от смены писателя (если запись пойдёт через app-копию или kit-писатель станет перезаписывающим), а не спасение данных. Комментарии переписаны под этот факт. Дрейн-ветка и NoProxyAvailableError строили dict вручную — переведены на {**_payload(), "interrupted": 1} и _payload(), чтобы «один _payload() на функцию» было правдой. Тест: к cancel добавлены ассерты на финальный heartbeat и mark_done — оба payload'а несут унаследованное ∪ пройденное этим прогоном. Refs #3355, PR #3363. Closes #3369 |
|||
| 5ad0d1a304 |
fix(scraper-kit/yandex): обрезанный потолком страниц leaf не идёт в чекпоинт
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m54s
После #3362 degraded-ветка отмечается complete=False, а `_leaf` писал бакет как полный, даже когда его пагинация упиралась в max_pages_per_bucket/_GATE_MAX_PAGES_CAP. С containment-гейтом (#3358/#3359) такой ключ покрывает свой интервал целиком, и резюм больше не заходит в полосу, чей хвост не читали ни разу. Флаг полноты — как у cian: complete = pages_needed <= max_pages, передаётся в on_bucket на обоих выходах leaf'а (_mark_bucket кладёт в done только complete). Переполненный leaf возможен только там, где бисекции дробить нечем (размах < min_bracket, открытый верхний брекет, потолок глубины) — это честный исход «бакет неполон по построению», поэтому он ещё и считается отдельно (scraper.capped_buckets → counters.capped_buckets): лечится не повтором прогона, а порогами бисекции. Closes #3368 |
|||
| b796702d9a |
fix(domclick): чекпоинт done_buckets во всех payload'ах свипа
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m5s
cancel-ветка run_domclick_city_sweep писала голый counters.to_dict(); писатель — app-level scrape_runs с полной перезаписью (CAST(:counters AS jsonb)), а 'cancelled' входит в _RESUME_STATUSES → отменённый прогон закрывался с пустым чекпоинтом и резюм пересобирал все корзины. Те же потери были у финального heartbeat и у mark_banned/mark_failed/mark_done (banned/failed тоже резюмируемы): они затирали чекпоинт, записанный после SERP-фазы. Один _payload() на функцию — done_buckets едет в каждой записи counters. Refs #3355, PR #3363. Closes #3369 |
|||
| 83946e5d4f |
Merge pull request 'fix(scraper-kit): дрейн-метка interrupted=1 у domclick sweep и трёх full-load — резюм после SIGTERM для всех функций с чекпоинтом' (#3363) from fix/3355-drain-mark-fullloads into main
Some checks failed
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 4m7s
Deploy Trade-In / build-backend (push) Successful in 1m47s
Deploy Trade-In / deploy (push) Successful in 3m12s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Failing after 12s
|
|||
| 12945d7b2f |
fix(scraper): дрейн domclick сохраняет чекпоинт, дрейн cian-detail помечается
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m0s
Ревью PR #3363: 1. domclick city sweep: комментарий обещал, что унаследованный при claim чекпоинт переживёт дрейн «jsonb-мержем» — мержа нет. domclick пишет counters через app-level scrape_runs (update_heartbeat/mark_done делают `counters = CAST(:counters AS jsonb)`, полная перезапись), а scheduler при claim done_buckets не наследует. Дрейн закрывал прогон с ПУСТЫМ чекпоинтом, резюм пересобирал все шесть корзин. Чтение чекпоинта поднято выше ранних выходов, дрейн-payload несёт `done_buckets` явно — как уже делает ban-ветка (except NoProxyAvailableError) той же функции. 2. cian full-load, detail-фаза: второй ранний выход по shutdown_requested() делал `break` и уходил в обычный mark_done — SERP целый, обогащение обрезано, а прогон выглядел полным. Теперь тот же sentinel RuntimeError("shutdown"), что и в _on_bucket: interrupted=1 + done_buckets. 3. yandex/avito full-load проверены: по одному shutdown-сайту в _on_bucket, detail-фазы нет вовсе — аналогичного дефекта нет. Тесты: чекпоинт дрейна domclick сверяется ПО ЗНАЧЕНИЮ в обеих записях (heartbeat + финализатор); дрейн cian в detail-фазе даёт interrupted=1. |
|||
| 29c0ae006d |
Merge pull request 'fix(scraper-kit): containment до пробы у cian и yandex — резюм full-load не пере-пробивает готовые полосы' (#3362) from fix/3359-containment-cian-yandex into main
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Has been cancelled
|
|||
| 4184f751bb |
fix(yandex): degraded-бакет не зачитывается как пройденный
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m11s
Ревью #3362: в _degraded (probe провалился → пагинация до пустоты, обрезаемая max_pages_per_bucket) yandex звал on_bucket БЕЗ признака полноты, и ключ падал в done-леджер наравне с честно добранным листом. До containment-гейта это был точечный skip одного ключа; теперь ключ покрывает ИНТЕРВАЛ и сливается со смежными — недобранная после отказа полоса больше никогда не переобходится. Тот же путь, что у cian: третий позиционный аргумент complete, в чекпоинт пишет только _mark_bucket(..., True); partial_buckets вынесен в счётчики прогона (виден в heartbeat), лоты и cancel/shutdown-проверки не трогаем. Плюс комментарий смежности в bisection.py приводил полуоткрытый пример, споря с «hi ВКЛЮЧИТЕЛЬНА» в том же докстринге. |
|||
| 3a0b94e052 |
fix(scheduler): мерить свежесть источника данными, а не статусом прогона
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m9s
`_STALE_SOURCES_SQL` считала свежесть возрастом последнего прогона со статусом 'done'. Прогон с блоком честно финализируется как 'banned' (#2657) и при этом вставляет строки: у domclick_city_sweep 886 строк 25.08 и 128 строк 23.08 — оба 'banned'. Источник, регулярно ловящий блок и столь же регулярно приносящий данные, числился мёртвым навсегда, отсюда ложный P1 #3118 «домклик не собирается с 5 августа». Запрос отдаёт завершённые прогоны, решение «прогон дал данные» принимает `run_brought_data` ТЕМ ЖЕ результатным словарём, которым уже судит сторож нулевого результата (runs._RESULT_COUNTER_KEYS, #2703) — одна мера на оба механизма. Не 'lots_inserted': это новизна, а не наличие данных (здоровый дедуплицированный sweep вставляет ноль). Результат не измерен (28 источников без результатного ключа) → судим прежней мерой, статусом: «не измерено» ≠ «ноль». never_ok считается той же мерой, иначе соврал бы в другую сторону. Closes #3172 |
|||
| f6cf948358 |
fix(scraper): пометить SIGTERM-дрейн interrupted=1 у full-load'ов и domclick-свипа
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m5s
После #3330/#3346 резюм берёт 'done' только с counters.interrupted=1, но четыре
функции с живым чекпоинтом done_buckets финализировали дрейн чистым 'done':
cian/yandex/avito full-load (ветка RuntimeError("shutdown")) и domclick city
sweep (дрейн до SERP). Оборванный обход был неотличим от полного, а собранные
бакеты никто не подхватывал — у avito это ещё и бан-бюджет (#3315).
done_buckets в domclick-payload не добавляем: чекпоинт унаследован при claim
(#3074), jsonb-мерж его сохраняет.
Closes #3355
|
|||
| 7322615868 |
Merge branch 'main' into fix/3312-defer-interval-minutes
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 14s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m34s
|
|||
| 63e2210abd |
fix(scheduler): defer уважает interval_minutes источника
Some checks failed
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 16s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 5m49s
_defer_next_run_at (путь пропуска pre_claim — например протухшие куки Циана) читал только interval_days, поэтому cian_detail_backfill с каденцией 360 мин после пропуска получал случайный час завтрашних суток: от ~1 до ~47 часов вместо шести. Ручное обновление кук не давало эффекта ещё сутки. Разбор interval_minutes вынесен в _interval_minutes и переиспользован post_claim-хуком reschedule_after_minutes — два входа в одну каденцию. Нижний порог _MIN_DEFER_MINUTES = 3 тика планировщика сохраняет #1522: defer обязан пережить несколько get_due_schedules, иначе pre-check снова гоняется каждую минуту. Refs #3312, #1522 |
|||
| ac11156f7d |
fix(scraper-kit): метка дрейна interrupted=1 во всех city-свипах (#3333)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m13s
После #3319 резюм подхватывает 'done'-прогоны только с counters.interrupted (_drained_done в scheduler._resume_decision), но ставил метку ровно один avito_city_sweep. У yandex, cian и newbuilding SIGTERM-drain финализировался чистым 'done' с частичными счётчиками: оборванный деплоем обход неотличим от полного и из резюма выпадал, хотя чекпоинт done_buckets есть у всех трёх (combo-метки / имена якорей / номера страниц). done_buckets в дрейн-payload не добавляю: heartbeat мержит jsonb, уже записанные единицы обхода переживают финализатор, а пустой список у multi-anchor yandex затёр бы унаследованный при claim чекпоинт. |
|||
| 41f21c4969 |
docs(scraper-kit): обоснование #3319 — что именно теряло чекпоинт
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m13s
Ревью опровергло формулировку «точку терял каждый финализатор»: все четыре писателя в runs.py мержат jsonb (`counters || :counters`), записанный ключ переживал mark_done/mark_banned/mark_failed. Правка закрывает выходы РАНЬШЕ первого end-of-anchor heartbeat (cancel/дрейн на первом якоре, ранний done #1950 на якоре №1) — комментарий и докстринг переписаны на это. Замер «0 из 67 за 60 дней» назван тем, чем он является: запись появилась 26.08.2026 (#3074) при такте avito 7 суток, выборка почти вся из эры без механизма. Плюс тест на прогон без ключей (эра до #3074) — метка дрейна не меняет вердикт «нечего подхватывать». |
|||
| 0ed934ea09 |
fix(scraper-kit): чекпоинт avito_city_sweep доживает до финализатора (#3319)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 14s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m30s
0 из 67 прогонов за 60 дней имели done_buckets в counters: точку писала одна строка внутри цикла якорей, а каждый выход (mark_done — включая ранний #1950 «SERP собран, detail заблокирован», — mark_banned, mark_failed) отдавал голый counters.to_dict(). Точка держалась только на jsonb-мерже в runs.py, то есть на свойстве чужого модуля, которого этот файл не проверяет. - payload любого выхода собирается одной функцией _ckpt() — done_buckets несут все 14 записей, а не одна; - якорь, умерший по таймауту, больше не считается пройденным (тот же инвариант, что у generic-except): SERP мог успеть, detail нет, и резюм пропускал такой якорь навсегда при штатно завершившемся прогоне; - SIGTERM-дрейн помечается counters.interrupted=1 и участвует в резюме. Статус остаётся 'done' — ни один читатель статуса не меняется; метка та же, что у rosreestr_dkp-дрейна. 'done' в _RESUME_STATUSES НЕ добавлен: чистый полный обход резюмить нечего. Дрейн перед IMV-фазой помечен отдельно (imv_phase_drained) — якоря там пройдены все, подхват собрал бы ноль. |
|||
|
|
44633b0df4 |
fix(tradein/domclick): свип ходил на QRATOR без кук и вис на PoW каждым запросом (#3264)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m53s
serp.py не передавал куки сессии вообще — слова cookie в файле не было. Свип приходил к QRATOR с чистым браузером и был обязан решать proof-of-work с нуля на каждый запрос. Прод, прогон 5330 (30.08 03:49-03:55): 9 запросов к bff-search-web.domclick.ru, все 9 зависли на челлендже (перезагрузка 1/2, 2/2, отказ), status=failed, 0 лотов. Прокси при этом ротировался (узел 9 → 10) — узел тут ни при чём. Добор с тем же сайдкаром и тем же пулом в ту же ночь взял 37 карточек из 37 без единого блока. Разница ровно в куках. Чинить это стало возможно только сейчас: свип ходит не на ekaterinburg.domclick.ru, а на отдельный хост bff-search-web.domclick.ru, и до правки _cookie_domain (PR #3262) куки легли бы на .bff-search-web.domclick.ru, не совпав с сессией площадки. Теперь оба хоста схлопываются в общий .domclick.ru. Снимок приходит параметром снаружи, а не читается внутри kit: kit не импортирует app.* (strangler-инвариант #2133). Поэтому джоба domclick_city_sweep переопределена продуктовым Handler'ом — build_registry это прямо допускает («последнее слово за продуктом»), а БД читает только app-сторона. Отсутствие сессии не авария: load_session вернул None → свип идёт как раньше, без инъекции, факт логируется один раз. Цена решения — override повторяет вызов kit-джобы целиком и может тихо с ней разойтись. Добавлен тест, который зовёт оба джоба одинаково и сравнивает наборы kwargs, допуская расхождение ровно в cookies. Проверен мутацией: с искусственно добавленным в kit-версию аргументом краснеет, без него зелёный. Тесты: 245 passed, 1 skipped (domclick + parity). |
||
| bdb9b64b03 |
Merge pull request 'fix(tradein/domclick): исчерпание пула прокси помечалось как отказ сбора (#3118)' (#3174) from fix/3118-domclick-no-proxy into main
All checks were successful
Deploy Trade-In / test (push) Successful in 4m0s
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 1m16s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
|
|||
|
|
ade1a065d6 |
fix(tradein/domclick): исчерпание пула прокси свипа теперь infra-бан, не отказ сбора
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m42s
NoProxyAvailableError поднимается из BrowserFetcher.__aenter__ (_acquire_lease)
ДО первого HTTP-запроса, когда пул прокси пуст — это НАША инфраструктура, не
блокировка площадкой. У run_avito_full_load/run_cian_full_load/run_yandex_full_load
уже есть выделенный except NoProxyAvailableError -> mark_banned(ban_kind='infra'),
у run_domclick_city_sweep его не было: исключение проваливалось в общий except
Exception внутри SERP-фазы, _scraper_ref оставался пустым, и честный статус ниже
видел "0 лотов + errors>0" -> mark_failed("fetch errors — 0 listings") с
ban_kind=NULL. Прод-факт: run 5023 (27.08) умер за 51 мс, errors_count=1,
ban_kind=NULL — неотличимо от честного отказа сбора площадкой.
Добавлен except NoProxyAvailableError перед generic except Exception (порядок
важен: класс — подкласс RuntimeError). Обработчик зеркалит avito/cian/yandex:
mark_banned + ban_kind_of_exception(exc) (даёт BAN_KIND_INFRA), и сохраняет
унаследованный чекпоинт (skip_buckets) вместо потери его на нашем же отказе.
Тест test_3118_domclick_no_proxy.py проверен на обратимость: без обработчика
падает (mark_failed вместо mark_banned), с обработчиком — проходит.
|
||
|
|
dc793e8701 |
fix(tradein/yandex): чекпоинт combo ставился до save_listings, не после
All checks were successful
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m40s
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
_on_combo в run_yandex_city_sweep делал done_combos.add(combo_label) ДО вызова save_listings. Отказ save_listings перехватывается (осознанно — одна упавшая единица не должна ронять весь sweep) и логируется, но combo уже был отмечен пройденным и уходил в heartbeat done_buckets. Следующий resume брал skip_combos из done_buckets (членство в множестве — само по себе корректно, не трогал) и пропускал этот combo навсегда: молча, прогон завершался штатно, просто сегмент выдачи не собирался никогда. Тот же инвариант "отмечаем пройденным только после успешного save", что уже есть у страницы в run_avito_newbuilding_sweep (_saved_ok), якоря в run_avito_city_sweep (_anchor_ok) и бакета в run_cian_full_load (_mark_bucket) — применил к combo. Heartbeat пишется в любом случае (и при отказе save тоже), иначе reap_zombies посчитает живой прогон мёртвым. Тесты: test_3170_yandex_combo_checkpoint.py — combo с упавшим save не попадает в done_buckets, успешный (включая пустую выдачу) — попадает. Обратимость проверена: с возвращённым дефектом (git stash) первый тест красный, со снятым — зелёный вместе с существующим test_3074_yandex_ sweep_checkpoint.py (6/6). |
||
|
|
5a410687ac |
feat(tradein/scraper): чекпоинты для avito_newbuilding_sweep — страница как единица (#3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m41s
Последний длинный свип без возобновления: при обрыве прогон начинался с первой страницы, а собранное терялось целиком — save_listings вызывался один раз на весь sweep. Единица возобновления — страница выдачи, по образцу якорей в city sweep. `_paginate_sweep`/`fetch_newbuildings` получили `start_page` (уже собранные страницы не запрашиваются) и колбэк `on_page`, который вызывается только после того, как страница пройдена до конца. Сохранение стало постраничным, номера пройденных страниц копятся в `scrape_runs.counters.done_buckets` мержем через `update_heartbeat`. Два инварианта, без которых фича вредна: 1. В чекпоинт попадает только страница, чьи лоты СОХРАНЕНЫ. Отказ save_listings перехвачен и прогон продолжается, но отметить такую страницу пройденной значило бы, что следующий прогон её пропустит и объявления оттуда не соберутся никогда — молча, потому что прогон завершится штатно. 2. Подхват начинается с ПЕРВОЙ несобранной страницы, а не с max+1. Дыра в чекпоинте возможна ровно из-за п.1, и max+1 перепрыгнул бы её навсегда. Страницы после дыры перечитаются — это дешевле потери и безопасно, повторная запись схлопывается по dedup_hash. Оба инварианта закрыты тестами, которые падают при их нарушении. |
||
| ede5653aba |
feat(tradein/domclick): чекпоинты для city_sweep — корзина как единица возобновления (#3118, #3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m43s
Прод-факты #3118: QRATOR рубит свип внутри 1-2-й корзины при любом старте (buckets_completed ≤ 1 из 6), banned-прогоны собирают 59–1815 лотов. Сдвиг #2854 лишь распределяет потери; чекпоинт превращает случайную ротацию в систематический обход — шесть прогонов закрывают шесть корзин. Моё раннее «домклику чекпоинтить нечего» (замер в #3074 по средним 3 мин) опровергнуто данными #3118 — этот коммит исправляет тот вывод кодом. Зеркально yandex-чекпоинту (#3074): провайдер — skip_buckets + имена завершённых корзин + гард «цепочка накопила все 6» (честный no-op); buckets_total сжимается до объёма ЭТОГО прогона; пайплайн — done_buckets = унаследованное ∪ завершённое, heartbeat с мержем jsonb до финализации; планировщик — generic _pick_resume (#2845). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 3c315b5105 |
fix(tradein/scheduler): boot-reap виден и при нуле — молчащий механизм неотличим от неподключённого (#3122)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m45s
Первая живая приёмка #3122: деплоевский startup-reap успел снять зомби раньше, boot-reap отработал с нулём — и не оставил НИКАКОГО следа исполнения. Ноль — штатный исход, но «сторож, молчащий при нуле» — это слепая зона по построению: оборванную проводку не отличить от чистого прода. INFO-строка при нуле, warning при снятых — как было. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 63956f0891 |
feat(tradein/scheduler): boot-reap — прогоны предыдущего контейнера снимаются на старте (#3122)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m40s
Прод-факт 27.08 (после ночного офлайна #3119): 4 прогона 'running' со стартами до старта контейнера блокировали свои источники через has_running_run до 6-часового порогового reap'а — до пяти часов слепоты на источник ровно после простоя, когда догон нужнее всего. Критерий — started_at < старт процесса планировщика (минус минута на дрейф), пульс не участвует: ложные срабатывания класса #2702 (редкий пульс длинных прогонов) невозможны по построению — живой прогон этого процесса не может быть старше самого процесса. Маркер counters.boot_reaped=true открывает boot-зомби подхват чекпоинта (_resume_decision): у порогового zombie процесс может быть жив (движущаяся точка — причина исключения 'zombie' из _RESUME_STATUSES), у boot-зомби — гарантированно мёртв. Пороговый zombie без маркера по-прежнему отвергается (закреплено тестом-инвариантом). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
|
|
68daac2a30 |
feat(tradein/scraper): чекпоинт по якорям для cian_city_sweep (#3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m43s
Третий шард после yandex (#3098) и avito (#3112). Выбран по замеру за 60 дней: 65 прогонов, среднее 35 минут, максимум 72, две отмены деплоем. Пятиминутного дренажа (#3029) на такие прогоны не хватает - убитый на 35-й минуте сбор начинался заново с первого якоря. Ключ чекпоинта - ИМЯ якоря, а не индекс: состав списка зависит от city_slug (областные свипы идут по своим наборам), позиция между городами не устойчива. По той же причине гарда по числу якорей не нужна - в отличие от combo-чекпоинта яндекса, где ключ якоря не содержал. Отличие от avito-шарда: там успех и неудача якоря сходились в одной строке и потребовался отдельный флаг _anchor_ok. У циана граница уже проведена самим потоком управления - все ветки отказа делают return или continue и до записи чекпоинта не доходят. Добавлять флаг значило бы дублировать то, что уже выражено структурой; достаточно писать чекпоинт в единственной точке успеха. Тест сторожит эту границу отдельно, потому что рефакторинг, сливающий ветки, сломал бы её незаметно. Пропущенный якорь двигает anchors_done - чтобы счётчик продолжал означать "докуда дошли по списку", а не "сколько собрал именно этот прогон". Тесты (4) поведенческие, с подменой CianScraper и save_listings: якорь из чекпоинта не опрашивается вовсе; пройденный дописывается поверх унаследованных; без чекпоинта обходятся все; упавший в чекпоинт не попадает. Двойнику пришлось добавить счётчики state_extraction_* - конвейер читает их после каждого якоря (#2625), и без них падал бы сам двойник, а не проверяемая логика. Фальсификация: на исходном коде краснеют все 4. Весь набор #3074 (yandex, avito, cian, claim) - 14 passed. |
||
|
|
0586951772 |
feat(tradein/scraper): чекпоинт по якорям для avito_city_sweep (#3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m47s
Продолжение после yandex-свипа. Выбор источника — по замеру, а не «для
полноты»: за 60 дней avito_city_sweep дал 68 прогонов, 25 банов и 3 отмены
деплоем при среднем времени 15 мин и максимуме 97.
Прод-факт, который решает дело. Типичный итог свипа:
{"anchors_done": 1, "anchors_total": 5, ...,
"enrichment_abort_note": "detail enrichment aborted (Avito detail
firewall/soft-block ...)"}
Прогон срывается блокировкой на ПЕРВОМ из пяти якорей. Без чекпоинта
следующий прогон снова идёт в первый якорь, упирается в ту же стену, и якоря
2-5 не собираются никогда.
Ключ чекпоинта — ИМЯ якоря, а не индекс: состав списка зависит от city_slug,
позиция в нём между городами не устойчива. По той же причине здесь не нужна
гарда по числу якорей, которая есть у combo-чекпоинта яндекса: там ключ
якоря не содержал, здесь якорь и есть ключ.
Инвариант, ради которого отдельный флаг _anchor_ok: в чекпоинт попадает
только якорь, пройденный до конца. Ветка блокировки делает return и до записи
не доходит, а generic-except доходит — якорь упал, но цикл продолжается.
Записать такой якорь пройденным значило бы, что следующий прогон пропустит
его навсегда, причём молча: прогон завершится штатно, просто часть города не
соберётся. Флаг сбрасывается на каждой итерации, иначе один упавший якорь
заразил бы все последующие.
Пропущенный якорь двигает anchors_done — чтобы счётчик продолжал означать
«докуда дошли по списку», а не «сколько собрал именно этот прогон».
domclick_city_sweep намеренно НЕ трогаю: 54 прогона, ноль отмен деплоем,
среднее время 3 минуты — чекпоинт там не окупается. cian_city_sweep (среднее
35 мин, 2 отмены) — следующий шард.
Тесты (4) поведенческие: якорь из чекпоинта не опрашивается вовсе; пройденный
дописывается поверх унаследованных; без чекпоинта обходятся все; упавший в
чекпоинт НЕ попадает. Оговорка: на исходном коде они падают по сигнатуре
(unexpected keyword argument), то есть доказывают отсутствие параметра, а не
поведение — поведенческую часть держат сами проверки. Весь набор #3074 —
10 passed.
|
||
| 993b44c7e0 |
Merge pull request 'feat(tradein): чекпоинты #3074 — наследование при claim + yandex_city_sweep' (#3098) from fix/3074-checkpoint-survives-claim into main
Some checks failed
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m46s
Deploy Trade-In / deploy (push) Has been cancelled
|
|||
| 307055232e |
feat(tradein/yandex): чекпоинты для yandex_city_sweep — combo как единица возобновления (#3074)
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m35s
Из таблицы убитых деплоем: yandex_city_sweep 15.08 прожил 65 мин, 12.08 — 2ч33м; оба потеряны целиком — у свипа не было чекпоинтов вовсе. Единица чекпоинта — combo (сегмент × комнатность × ценовой диапазон), ровно то, чем цикл обхода уже итерируется. Три слоя: - провайдер: skip_combos (ни одного HTTP по собранным) + on_combo для каждого ПРОЙДЕННОГО combo, включая пустые — иначе пустой combo не попадал бы в чекпоинт и перечитывался бы вечно; оборванный отказом combo (gate failure) on_combo по-прежнему не вызывает; - пайплайн: done_combos → heartbeat с done_buckets (мерж jsonb, финализаторы не затирают); подхват гейтится единственным якорем — combo_label не содержит якоря, multi-anchor подхват пропускал бы чужие якоря; - планировщик: resume_run_id=_pick_resume(...) в диспатче (generic-механизм #2845 — params-идентичность, свежесть точки, потолок цепочки — бесплатно). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| e4b3c6cc2b |
fix(tradein/scheduler): чекпоинт предшественника переживает обрыв до первой новой корзины (#3074)
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m35s
Прод-факт: прогон 4707 (23.08) подхватил 42 корзины у 4117, был убит деплоем на 26-й минуте до завершения первой НОВОЙ корзины — и не успел ни разу написать heartbeat с done_buckets. Его собственный чекпоинт пуст: следующий кандидат увидел бы no_checkpoint, цепочка оборвалась бы с потерей 42 корзин. _resume_decision при вердикте 'ok' теперь кладёт done_buckets предшественника в counters-заготовку нового прогона — она персистится при claim, до старта пайплайна. Heartbeat мержит jsonb: первый настоящий bucket-heartbeat перезапишет ключ надмножеством, двойной записи нет. Отказные вердикты чекпоинт не наследуют (прогон с нуля не должен врать о собранном). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 0a85fa7202 |
fix(tradein/observability): результат detail-бэкфиллов доезжает до колонки total_seen (#3044)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m37s
Прод-факт: замер по avito_detail_backfill прочитал «обогащено 0 за 7 дней» при реальных 801 — джобы пишут результат только ключом 'enriched', которого _column_counts не знал, и колонка total_seen оставалась 0 у всех прогонов. 'enriched'-фолбэк добавлен именно в _column_counts (витринная колонка), а НЕ в _RESULT_COUNTER_KEYS: тот список кормит zero-result-стрик, где догнавший очередь бэкфилл стал бы непрерываемым измеренным нулём — ловушка, за которую ревью уже выкинуло из списка 'rows_inserted' (#2703). Контракт стрик-сторожа закреплён инвариант-тестом. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
|
|
fc2930fbb6 |
Merge remote-tracking branch 'forgejo/main' into fix/3034-avito-fingerprint
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m28s
|
||
| 7382ae123d |
feat(tradein/avito): полный обход качает вторичку родным фильтром пути (#3033) (#3039)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m42s
Deploy Trade-In / build-backend (push) Successful in 1m48s
Deploy Trade-In / deploy (push) Successful in 2m28s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
|
|||
|
|
fe628a5e17 |
fix(tradein/scraper-kit): actualize avito fingerprint - impersonate/Sec-Fetch-Site/referer/cookie-check
Живой замер браузера 2026-08-21 разошёлся с тем, что шлёт прогретая сессия:
- impersonate="chrome120" был захардкожен в 9+ местах (avito/{serp,detail,imv,
houses}.py, pipeline.py x4, cian/valuation.py, yandex/valuation.py) вместо
единой DEFAULT_IMPERSONATE (providers/_base.py). Обновление до chrome146
(макс. доступный профиль curl_cffi 0.15.0; alias "chrome" НЕ используется -
едет сам при апгрейде библиотеки без ревью) теперь меняется в одном месте.
Guard-тест backend/tests/test_impersonate_single_source.py грепает всё
дерево scraper_kit на литерал "chrome120".
- DOCUMENT_HEADERS ставил Sec-Fetch-Site="none" на уровне сессии, а Referer
добавлялся per-request (curl_cffi мёржит per-request headers поверх
session-level) - живой Referer с "none" рядом не бывает у настоящего
Chrome. Добавлен referer_headers() (Sec-Fetch-Site="cross-site") для
caller'ов с чужедоменным Referer; avito warm-up (yandex/ya.ru -> avito)
теперь его использует. Внутренний avito-search -> avito-detail Referer
(fetch_detail, same-origin) НЕ тронут - отдельный явный комментарий почему.
- Referer прогрева заменён с yandex.ru на ya.ru (живой переход из выдачи
даёт короткий домен). ysclid сознательно не добавлен - значение выпускает
Яндекс, подделка хуже отсутствия.
- warm_up_session/research_in_session считали прогрев успешным по HTTP-
статусу и отсутствию firewall-маркеров, не проверяя антибот-cookies
(__zzatw-*/cfidsw-*, сняты с живого браузера) - detail-батч на такой
"прогретой" сессии сжигал прокси на обречённых 403. Теперь поднимают
AvitoWarmupCookiesMissingError (наследник AvitoBlockedError - существующие
except-блоки/ban_kind/proxy-ротация в pipeline и backfill ловят без
изменений).
Полный backend pytest suite зелёный (4672 passed), ruff check чист.
Refs #3034
|
||
| b9cfdaa040 |
feat(tradein): secondary_only — параметр расписания, выброшенное считается (#1781)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m27s
`run_cian_full_load` передавал `secondary_only=True` жёстко, поэтому включить новостройки в полный обход можно было только деплоем. Теперь это параметр с ТЕМ ЖЕ дефолтом `True` — поведение прода не меняется ни на строку, но решение становится правкой одной ячейки `scrape_schedules.default_params`, а не выкаткой кода. Откат — тем же движением. Почему это важно именно здесь. Новостройки НЕ пропускаются при запросе: они скачиваются, разбираются и выбрасываются последним шагом (`cian/serp.py`), потому что SERP-параметр `object_type=1` у Cian ненадёжен (~5 % выдачи) и фильтруют по authoritative `listing_segment` после парсинга. Проба бакета берёт `totalOffers` из Redux-состояния SERP и считает `pages_needed = ceil(totalOffers / offers_per_page)`, а `totalOffers` включает ОБЕ категории — то есть страницы с новостройками уже скачаны, лимит страниц и антибан-бюджет за них уже заплачены. Включение стоит ноль дополнительных запросов. Заодно `dropped_novostroyki` сохраняется в counters прогона. Счётчик логировался (`dropped_nb=`), но не персистился, и ответить «сколько инвентаря выбрасывает полный обход» задним числом было нечем: логи за 17.08 уже ротировались — `docker logs --since 120h` не находит ни строки «cian:» ни в одном контейнере. Тот же довод, по которому рядом заведён `partial_buckets`. Копится в атрибуте инстанса, а не аргументом `on_bucket`: у колбэка есть внешние реализации, менять его сигнатуру ради счётчика нельзя. Сброс на каждый прогон — инстанс переиспользуется. Замер, ради которого это делается (прод 21.08): месячный охват свипа cian/novostroyki — 11.7 % против 100 % у cian/vtorichka и avito/novostroyki; 11 993 активные строки, медианный возраст 81 сутки, 10 585 старше 30 суток. Подробности и оговорки — в #1781 и #2994. Двусторонне: против origin/main пять тестов красные, и краснота везде по значению, а не по отсутствию символа — ни одного KeyError. Сообщения перечисляют фактическое состояние («параметра нет в сигнатуре; параметры: [...]», «поля нет в запросе; поля: [...]»). Контроли зелёные с обеих сторон: дефолт остаётся True (иначе правка тихо включила бы сбор новостроек на проде — это отдельное решение с замером); фильтр при `secondary_only=True` остаётся на месте и по-прежнему зависит от флага. pytest tradein-mvp/backend — 4644 passed, 23 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| a9e9707018 |
fix(tradein): обход Домклика начинается со сдвигом — комнатности 2+ перестают быть слепой зоной (#2854) (#2932)
All checks were successful
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m45s
Deploy Trade-In / deploy (push) Successful in 1m57s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-backend (push) Successful in 1m49s
|
|||
|
|
e9ca744e85 |
fix(tradein/scrapers): не путать rows_inserted/processed с честным результатным ключом
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m4s
Ревью честного run-status нашло, что _RESULT_COUNTER_KEYS ловил не только целевой yandex_newbuilding_sweep, но и rosreestr_dkp_import (rows_inserted, 66 из 67 прод- прогонов = здоровый ноль догнавшего инкрементального импорта) и newbuilding_enrich (processed — счётчик попыток, ==limit даже при частичном провале). Первое завело бы практически непрерываемый ложный zero-стрик у здорового источника, второе маскировало бы реальные отказы под measured-N. Проверено по прод-БД (2026-08-15): "succeeded" пишут ТОЛЬКО yandex_newbuilding_sweep (42 прогона/90д) и newbuilding_enrich (65/90д) — ни разу rosreestr_dkp_import; у yandex_newbuilding_sweep succeeded численно совпадает с rows_inserted на всех 42/42 прогонах. Заменил "rows_inserted"+"processed" на "succeeded" в _RESULT_COUNTER_KEYS (app-копия и byte-эквивалентная kit-копия) — цель (b) исходной правки сохранена, ложный стрик у rosreestr_dkp_import снят, попутно newbuilding_enrich получает честное измерение вместо счётчика попыток. Также поправлены докстринги test_backfill_honest_status.py — два кейса (76%/72% отказов -> 'done') проверяют только выбор финализатора mark_backfill_finished (mark_done там замокан); реальный mark_done с honest-run-status переквалифицирует их в 'failed' через _failed_ratio_too_high — это не документировалось явно. |
||
|
|
885031420e |
fix(tradein/scrapers): honest run status — стоп 'done' поверх провала и нуля
Три прод-факта, где status='done' врал о реальном исходе прогона:
- avito_detail_backfill 15.08: {"attempted":64,"failed":57,"enriched":6,"blocked":1}
-> 'done'. mark_backfill_finished звал mark_done, потому что produced=6 (>0);
ни _sweep_run_did_nothing (нет anchors_total/errors_count у backfill'ов), ни
_phase_totally_failed (голые "attempted"/"failed" без фазового префикса) эту
форму counters не ловили. Новый _failed_ratio_too_high внутри mark_done:
failed/attempted >= 0.5 -> 'failed', >= 0.15 -> тоже 'failed' (другая
формулировка причины в error-тексте) — 'partial' статусом не заведён: это
потребовало бы DROP+ADD CHECK constraint (051_scrape_runs_extend.sql) и
дообучения ещё 4 мест (Literal-фильтр admin API, статусы фронта, оба
IN-списка сторожей) — тот же класс проводки, что и у ban_kind (#2686/#2764),
который сознательно не стал новым статусом.
- yandex_newbuilding_sweep 26.07-10.08: десять прогонов подряд 'done' при
processed=5 succeeded=0 rows_inserted=0 failed_resolve=4-5 — сторож нулевого
результата (_alert_if_consecutive_zero_results) не видел ни один результатный
ключ этого sweep'а и молчал навсегда. _RESULT_COUNTER_KEYS дополнен
rows_inserted/processed (именно в этом порядке — rows_inserted это результат,
processed это попытки; иначе "5 обработано, 0 записано" замаскировалось бы
под measured-5).
- admin-витрина показывала new_count=0 у трёх подряд cian_full_load при реально
сохранённых saved_inserted=482/214/239 — full-load'ы не пишут ни 'new_count',
ни 'lots_inserted'. _column_counts дополнен saved_inserted/rows_inserted.
Правки продублированы в scraper_kit/orchestration/runs.py (byte-эквивалент
app.services.scrape_runs, см. докстринг модуля) для параллели: единственный
текущий писатель "attempted"/"failed" (mark_backfill_finished) живёт только в
app-копии, но приоритет ключей/константы держим синхронными на будущее.
Не тронуто: сознательно пустые sweep'ы (errors_count=0, honest empty) и малые
батчи (attempted < 3) — доля отказов на них не считается диагнозом.
Tests: tests/test_honest_run_status_failed_ratio.py (41 кейс, оба модуля,
включая точные прод-числа из трёх фактов выше) + regression-прогон 609 тестов
по всем файлам, трогающим scrape_runs/orchestration.runs — 0 регрессий.
|
||
| 01b5e73ea4 |
feat(tradein): вся Свердловская область — 40 городов в city-sweep (#2879)
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m33s
Deploy Trade-In / build-backend (push) Successful in 1m41s
Deploy Trade-In / deploy (push) Successful in 1m38s
|
|||
| 4b527751b9 |
fix(tradein/domclick): распознанный QRATOR-блок свипа — ban_kind='platform', не 'unknown' (#2687) (#2832)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m27s
Deploy Trade-In / build-backend (push) Successful in 1m37s
Deploy Trade-In / deploy (push) Successful in 2m49s
|