Commit graph

233 commits

Author SHA1 Message Date
f4d174283a chore(#3197): cian-login — без холостой аренды и ложного отказа (/login сайдкара override не берёт); честные докстринги
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 5m7s
Code-review хвоста #3197: на `cian-login` аренда из пула не доходила до сайдкара.
`BrowserFetcher._post_login` не кладёт `payload["proxy"]` (это делают только
`fetch`/`fetch_json`), а на приёме `login_handler` (browser/server.py:2814-2817)
зовёт `_no_live_proxy(provider, None)` и `_ensure_browser(provider)` без override —
`/login` proxy-override не принимает вовсе. Lease брался в `__aenter__` и
освобождался в `__aexit__` без пользы и без health-вердикта по узлу.

Хуже холостого хода: при пустом пуле в production `_acquire_lease` поднимает
`NoProxyAvailableError` ДО POST, `cian_auto_login` ловит любое `Exception` →
`502 Browser login failed`. То есть единственная ручка ВОССТАНОВЛЕНИЯ cian-сессии
отказывала ровно во время инцидента с пулом. Комментарий в admin.py при этом
утверждал, что фикс закрывает InvalidIP на логине — неправда.

Убран `proxy_provider=` (фабрика остаётся ради endpoint/environment из одного
места). `use_pool` без провайдера фетчер игнорирует сам — `_acquire_lease`:
`use_pool AND provider is not None` — поэтому ни аренды, ни прод-отказа.
`domclick-detail-debug` не тронут: он ходит через `fetch`, где override реально
кладётся в тело и читается сайдкаром — там #3197 остаётся настоящим фиксом.

Тесты для cian обратные по значению и падают на HEAD ветки:
`test_cian_auto_login_does_not_lease_from_pool` (провайдер не передан, `acquire`
не вызван) и `test_cian_auto_login_survives_empty_pool_in_production` (пустой пул
на проде не отдаёт 502). Стаб cian — подкласс настоящего `BrowserFetcher`, чтобы
второе утверждение шло через реальный `_acquire_lease`, а не через заглушку.

Follow-up (отдельной задачей): сайдкар `/login` не принимает proxy override →
логин cian всегда с env-узла (сейчас выключенный узел 9).
2026-09-06 17:48:29 +05:00
8761602e9b chore(tradein/proxy): последние две ручки admin.py — через фабрику фетчера; снят форс pool-режима у cian-history (#3197, #3386)
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 5m5s
Два хвоста одной темы — проводка пула прокси в контейнере backend.

#3197: `cian-login` и `domclick-detail-debug` были последними прямыми
конструкциями `BrowserFetcher(source=, endpoint=)` мимо `build_browser_fetcher`.
Без `proxy_provider`/`use_pool`/`environment` сайдкар брал свой env-узел
`SCRAPER_PROXY_URL` (на проде выключенный узел 9: 407 → camoufox `InvalidIP`), а
прод-отказ «пул пуст» (#2616) на этих путях был мёртв — он смотрит на
`environment`, который до конструктора не доезжал. Соседи по эпику уже переведены
(#3382 cian, #3389 yandex). Прямых конструкций без провайдера вне тестов больше
не осталось: остальные (backfill-задачи, pipeline) пул получают своими kwargs,
а `endpoint=None`-ветки providers — это документированный `config=None` для
офлайн-тестов.

#3386: `_PoolCurlConfig` в `cian_price_history` форсил `use_proxy_pool_curl=True`,
потому что у контейнера `backend` не было переменной. #3387 задал
`USE_PROXY_POOL_CURL: "true"` сервису `backend` в compose — зашитая константа
стала лишней и делала рубильник неотключаемым ровно на этом пути (докстринг при
этом описывал уже неверную причину). Теперь `RealScraperConfig()` напрямую.

Тесты меряют значения, а не наличие kwarg'а: на откате исходников красные
4 параметризации нового `test_3197_admin_debug_browser_pool_wiring`
(`assert None is not None` — провайдер не передан) и
`test_price_history_honours_flag_off` (`assert ['cian'] == []` — пул дёргался при
выключенном флаге).
2026-09-06 17:22:46 +05:00
7d3c0eab51 fix(#3398): acurl_proxy_url — вторая отмена не бросает lease; тест меряет наш release, а не teardown; потолок бюджета в докстринге
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 / 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 5m11s
2026-09-06 16:49:12 +05:00
e4feadefe6 fix(tradein/proxy): операции пула прокси уходят с event loop публичного /estimate (#3398)
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 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 5m8s
Вход в `providers/_proxy.py::curl_proxy_url` синхронный и стоял ДО первого await во
всех трёх async-сайтах `/estimate` (avito/imv, yandex/valuation, cian/valuation).
`RealProxyProvider.acquire/mark_health/release` ходят в БД синхронно (своя SessionLocal
на операцию), а публичный backend крутится на ОДНОМ воркере uvicorn (#3083): на
cache-miss это 3 источника x (acquire + mark_health + release) блокирующих вызовов
прямо на loop'е. `_with_budget(asyncio.wait_for)` синхронный вход прервать не может, а
при исчерпанном пуле коннектов (5+10) checkout ждёт до 30 с — весь инстанс молчит.

`acurl_proxy_url` — async-обёртка над тем же синхронным контекстом: вход и выход через
`asyncio.to_thread`, выход тоже (иначе mark_health/release держали бы loop на выходе).
Семантика прежняя: `NoProxyAvailableError` до запроса, BaseException-ветка (#3397:
отмена → health=False), release в finally. `to_thread` копирует contextvars, поэтому
`current_run_id` (#3404) виден в потоке как раньше.

Новое по сравнению с sync-путём: отмена может прийти ВО ВРЕМЯ acquire (раньше это было
невозможно по построению). Вход держится через `asyncio.shield` и добирается в except —
иначе выданная в потоке аренда висела бы до reap_stale_leases.

Переключены только три сайта `/estimate`. Sync-вызывающие и async-сайты под scheduler
(cian/detail.py::fetch_detail, cian/newbuilding.py::resolve_cian_zhk_url_via_search)
остаются на `curl_proxy_url` — там loop не обслуживает публичные запросы.
2026-09-06 16:05:50 +05:00
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
2026-09-06 10:40:11 +00:00
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) нет.
2026-09-06 15:30:12 +05:00
bot-backend
f4099cb89a Merge remote-tracking branch 'forgejo/main' into feat/3404-proxy-run-attribution
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 5m4s
2026-09-06 13:05:38 +03:00
bot-backend
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
2026-09-06 13:05:20 +03:00
957e9e8c01 fix(#3402): капча = «Captcha…» и «Вы не робот?» (бан+ротация); «Ошибка - Циан» — только лог на цикл наблюдения
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 Trade-In / backend-tests (pull_request) Successful in 5m8s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 1m27s
Ревью (⚠️ minor) на #3403: «Ошибка - Циан» может быть транзиентной 5xx-страницей,
отданной с кодом 200, а не отказом конкретному узлу. Цена ошибки несимметрична —
mark_banned эскалирует TTL до часов, поэтому 20-минутный сбой площадки выбил бы из
выдачи весь пул. Один список маркеров этого различить не мог: и капча, и страница
ошибки шли одним путём в BanPageDetectedError.

Проба прода 06.09.2026 09:25 UTC (одна карточка по узлам через сайдкар):
* узел 14, час назад отдававший «Captcha - база объявлений ЦИАН», вернул НАСТОЯЩУЮ
  карточку — капча снимается за 1-2 часа, то есть TTL бана по назначению;
* узел 1 отдал ТРЕТИЙ вариант отказа — `<title>Вы не робот?`, 16 КБ (час назад —
  «Ошибка - Циан», 374 КБ). Прежние маркеры его не знали вовсе: отказ уезжал наверх
  как валидный HTML ровно так же, как до #3402.

Маркеры разделены на два класса, одинаково в обоих слоях (образы backend и browser
деплоятся раздельно и расходятся на часы):
* КАПЧА — «captcha - база объявлений циан» + «вы не робот?»: безусловный отказ
  площадки, прежний путь (сайдкар → BanPageDetectedError → 403 + ban_page; kit →
  report_platform_ban + CianBlockedError). За ней нет контента, и узел, которому её
  показали, будет получать её дальше;
* «ошибка - циан» — ТОЛЬКО ЛОГ: сайдкар отдаёт HTML клиенту как есть и пишет WARNING
  «страница ошибки Циана (title=…, upstream=…) — не бан, только лог (#3402)», kit при
  провале extract_state пишет WARNING и возвращает прежний None. Ни бана, ни рапорта,
  ни исключения — решение принимаем по частоте в логах за цикл наблюдения, а не по
  догадке о природе страницы.

Нормализация заголовка прежняя (регистр/пробелы/тире). `_is_cian_refusal` →
`_is_cian_captcha` + `_log_cian_error_page`; `_refusal_title` → `_page_title` и два
кортежа маркеров рядом.

Фальсификация: «вы не робот?» убран из маркеров обоих слоёв → kit 1 failed
(«DID NOT RAISE CianBlockedError»), сайдкар 3 failed («DID NOT RAISE
BanPageDetectedError», `_is_cian_captcha` → assert False is True). Маркер возвращён,
обе сьюты зелёные: backend 5599 passed / 35 skipped, browser 246 passed.
2026-09-06 14:55:45 +05:00
ff979d4afd fix(tradein/cian): капча Циана с HTTP 200 — отказ площадки в обоих слоях (#3402)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 / browser-tests (pull_request) Successful in 1m28s
CI Trade-In / backend-tests (pull_request) Successful in 5m11s
Циан отдаёт капчу (`<title>Captcha - база объявлений ЦИАН`, 44 КБ) и страницу
ошибки (`<title>Ошибка - Циан`, 374 КБ) с кодом 200. Детектор сайдкара их не знал
(_REFUSAL_STATUSES {403,429} + маркеры Авито/Домклика), HTML уезжал клиенту как
успех, extract_state возвращал None и провайдер печатал «defaultState extraction
failed» — отказ ПЛОЩАДКИ читался как дрейф НАШЕЙ разметки. Аренда при этом не
менялась: fetch() уже отрапортовал mark_health(ok=True), fail-streak обнулялся, и
один капча-узел сжигал батч целиком (6200: 0/210; 6123/6091/6052/6032/6010/5981:
0/400 — против 161/162 через здоровый узел на прогоне 13).

Два слоя, потому что образы backend и browser деплоятся раздельно и расходятся
на часы:
* сайдкар (browser/server.py) — детект по <title> на обоих путях (navigate и
  подзапрос) → BanPageDetectedError → прежний путь #3288/#3379: 403 + ban_page +
  ЧЕСТНЫЙ upstream-статус 200;
* kit (providers/cian/detail.py) — при провале extract_state те же маркеры →
  CianBlockedError вместо тихого None, плюс report_platform_ban по живому lease.
  Там же ветка SidecarBanPageError: отказ, опознанный сайдкаром, больше не
  гасится общим `except` в «не смогли разобрать».

Слово `captcha` признаком быть не может: в нормальной карточке оно встречается 11
раз, на капче 17. Детект по <title> с нормализацией тире.

`_report_platform_ban` → `report_platform_ban` (публичный): тем же путём обязан
идти отказ, распознанный не сайдкаром, а провайдером. report_ban один только
пишет бан пары «узел×источник» — сменить сожжённую аренду ВНУТРИ батча позволяет
только fail-streak (_LEASE_ROTATE_AFTER_FAILS).
2026-09-06 14:10:57 +05:00
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, утверждавшие про
живого «перезаписывающего двойника», приведены в соответствие.
2026-09-06 11:45:56 +05:00
075cec4b57 Merge pull request 'fix(tradein/estimator): IMV-путь /estimate через пул прокси; пустой пул — честная деградация без IMV (#3386 п.в)' (#3397) from fix/3386-estimator-imv-proxy-pool into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
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 4m2s
Deploy Trade-In / build-backend (push) Successful in 1m39s
Deploy Trade-In / deploy (push) Successful in 1m14s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
2026-09-06 06:04:07 +00:00
33cad4f82e fix(#3386): curl_proxy_url — health=False и на отмене (BaseException); admin IMV через пул; тест без create=True
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
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 / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m57s
2026-09-06 10:57:08 +05:00
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
2026-09-06 05:53:37 +00:00
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
2026-09-06 10:15:43 +05:00
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
2026-09-06 10:07:29 +05:00
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» — бан площадки.
2026-09-06 09:45:56 +05:00
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 — по ней и идёт отбор.
2026-09-06 09:42:27 +05:00
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).

Тесты — по значению, не по факту вызова; на исходниках 30e3bacc краснеют все пять:
'listings_processed' дописан в финализированную строку (kit), KeyError: 'interrupted'
(app), assert 'running' == 'done' (отмена в теле тика), assert 0 == 1 (второй run_id
не помечен после отказа первого), RuntimeError наружу (недоступная БД).
2026-09-06 09:03:56 +05:00
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'ов).
2026-09-06 07:56:35 +05:00
939a8a1646 fix(#3197): no_proxy_stop=1 как у соседей, INFO на финале хендлера, докстринг фабрики
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 4m57s
2026-09-06 06:27:40 +05:00
cb5714fff0 fix(#3197): yandex newbuilding — обе точки в пул прокси + стоп на пустом пуле
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 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 4m56s
`YandexNewbuildingScraper.fetch_jk` и `resolve_yandex_jk_slug` строили
`BrowserFetcher(source="yandex", endpoint=...)` без proxy_provider/use_pool/
environment — сайдкар брал env-узел SCRAPER_PROXY_URL, на проде выключенный
(407 → camoufox InvalidIP → /fetch 503, факт #3386), то есть путь шёл мимо пула
целиком, а прод-отказ «пул пуст» (#2616) был мёртв: он смотрит на environment,
который до конструктора не доезжал. Обе точки собраны через
build_browser_fetcher(config, "yandex", proxy_provider=...), как yandex/serp.py.

Второе: общий `except Exception` в обеих функциях глотал NoProxyAvailableError и
возвращал None — прогон, не ходивший к площадке, перебирал все ЖК и уходил в
'done'. Теперь «пул пуст» пробрасывается наружу (caused_by_no_proxy), sweep
обрывается на первом доме с no_proxy_stop, а прогон финализируется как failed
(образец дефекта — #3382, cian/detail.py).
2026-09-06 05:55:36 +05:00
33444b7eab Merge pull request 'fix(tradein): гейт против сдвига разряда (×10) в offer_price_history у всех писателей + миграция 286 (#3376)' (#3383) from fix/3376-price-history-decimal-slips into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / test (push) Successful in 4m4s
Deploy Trade-In / build-backend (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 1m32s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
2026-09-06 00:19:33 +00:00
1b86ba7e06 fix(#3376): первая точка — только по третьей точке истории; числа под критерием
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 4m58s
2026-09-06 05:12:30 +05:00
721ceb9876 fix(tradein): выборка миграции 286 повторяет гейт 1:1, правило первой точки (#3376)
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 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 4m56s
Ревью нашло, что миграция и код ловили РАЗНОЕ. Миграция брала базой предыдущую
СЫРУЮ строку (lag), гейт — предыдущую ОСТАВЛЕННУЮ. На 1M→10M→1M→10M (цена 1M)
lag-версия удаляла честную точку, на 1M→10M→1.05M→9.9M — не была идемпотентной
(второй прогон доедал 9.9M). Теперь кандидаты выбирает PL/pgSQL-цикл, пошагово
повторяющий drop_decimal_slips, а правило первой точки — отдельным INSERT..SELECT
уже по ОСТАВШИМСЯ строкам.

Правило первой точки — из прод-разбора: 12 из 20 остатков domklik это серии вида
330 000 → 3 300 000 (текущая цена 3 300 000) и 420 000 → 4 200 000 → 4 500 000,
где дефектная точка ПЕРВАЯ и базы слева у неё нет. Свидетелей по-прежнему два:
×10 ко второй точке И подтверждение второй третьей-или-текущей-ценой. Решение по
первой точке принимается по kept-серии, а не по сырой, — иначе гейт теряет
идемпотентность (перебор ловит 1122 таких прогона).

Идемпотентность доказана НА ГЕЙТЕ: property-тест gate(gate(s)) == gate(s) по всем
сериям длины 2-6 (19 525 серий × 3 текущие цены). Фальсифицирован обеими
поломками — сырая база даёт 136 красных прогонов, сырые соседи первой точки 1122.
Раз SQL зеркалит гейт, свойство переносится на миграцию.

Ещё в 286: третий свидетель ПРОТИВ удаления (цена подтверждена триггерной строкой
того же объявления — значит она реально наблюдалась в listings.price_rub) и
финальный шаг |diff_percent| > 100 → NULL по всем источникам, то же правило, что
validate_diff_percent на записи. Ожидаемое число удалений в шапке — 35 + ~12 из
двухсвидетельского предзамера, а не 76 (то была односвидетельская цифра).
Прогон обеих фаз дважды с ROLLBACK — tradein-mvp/scripts/sql/286_dryrun.sql.

yandex: проводка гейта снята как мёртвая. На том пути серия из двух точек, а
свидетель последней — текущая цена лота, то есть она же сама: ветка по построению
не могла выбросить ничего. Оставлен честный комментарий-потолок и ссылка на
follow-up (отлов требует DELETE на следующем наблюдении).

cian: после выброса точки соседу пересчитывается diff_percent (было только у
domclick). domclick: цена листинга берётся RETURNING'ом у UPDATE вместо отдельного
SELECT по PK, пересчёт вынесен в общий recompute_diff_percent с гейтом на пустую
цену (ручной ingest кладёт price_changes из JSONL без валидации).
2026-09-06 03:53:08 +05:00
b21b1a928e Merge pull request 'fix(tradein/cian): cian_history_backfill идёт через пул прокси + стоп на пустом пуле (#3197, точка 1 из 3)' (#3382) from fix/3197-cian-history-backfill-proxy-pool into main
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 4m0s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 1m36s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-09-05 22:13:09 +00:00
0c21e522c0 fix(#3197): пустой пул пробрасывается из cian fetch_detail; тест через настоящий fetch_detail
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 4m58s
2026-09-06 03:05:32 +05:00
ea4fb88b66 Merge pull request 'fix(scraper-kit/yandex): full-load on_bucket отдаёт список лотов бакета, а не int (#3375)' (#3378) from fix/3375-yandex-fullload-on-bucket-contract 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) Successful in 2m28s
Deploy Trade-In / build-backend (push) Successful in 2m19s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / test (push) Successful in 4m10s
Deploy Trade-In / deploy (push) Successful in 2m43s
Deploy Trade-In / deploy-status (push) Successful in 1s
2026-09-05 21:46:35 +00:00
a5f60eaaa8 test(#3288): фикстуры бан-страницы на 403 и 500, фальсифицируемые проверки текста; комментарий про путь текста
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 / browser-tests (pull_request) Successful in 1m32s
CI Trade-In / backend-tests (pull_request) Successful in 5m7s
2026-09-06 02:38:29 +05:00
142967d064 fix(tradein): гейт против сдвига разряда в offer_price_history (#3376)
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 5m4s
В priceHistory источников встречаются точки ровно ×10/÷10 к соседям с
возвратом к базе следующей же точкой — это потерянный разряд у источника,
а не рынок. Прод-замер 06.09.2026: 76 таких строк у 55 объявлений
(domklik 48, cian 21, yandex 6; единственная avito-строка — триггерная).
Читатели колонки — медианный торг лендинга (#3223), админка, /scrapers.

drop_decimal_slips живёт рядом с validate_diff_percent — на той же единой
границе записи, что и гейт #3225, и подключён ко ВСЕМ писателям истории
(domclick/detail.py, cian/detail.py, yandex_price_history.py). Критерий
требует двух свидетелей: скачок ×10 к предыдущей точке И возврат к базе у
следующей; у последней точки серии свидетель — текущая цена объявления,
нет и её → точку не трогаем (без второго свидетеля ×10 может быть честной
сменой цены). У domklik после выброса пересчитывается diff_percent
соседа: парсер считал его от базы, которой больше нет.

Миграция 286 чистит уже собранные строки тем же критерием и только у
загрузчика (change_time <> recorded_at): триггерные строки — это живые
смены listings.price_rub, у них другая база отсчёта (см. 285). Падает,
если кандидатов больше 200.
2026-09-06 02:35:39 +05:00
8129e21b95 fix(tradein/cian): суточный бэкфилл истории — в прокси-пул, а не в env-прокси (#3197)
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 4m51s
BrowserFetcher(source="cian") в cian_history_backfill конструировался без
proxy_provider/use_pool/environment — трёх аргументов, которые кладут "proxy" в тело
POST /fetch. Сайдкар брал свой env-прокси (SCRAPER_PROXY_URL): пул из 4 узлов, его
баны и ротация проходили мимо, а прод-отказ «пул пуст → не ходить на env/direct»
(#2616) на этом пути был мёртв, потому что смотрит на environment. Проводка теперь
как у соседей — domclick_detail_backfill и house_imv_backfill.

Ожившему отказу нужен обработчик: NoProxyAvailableError ловился общим except на
объявление, и батч крутил впустую весь список (пул пуст с первого — значит пуст и на
1000-м). Распознаём по цепочке причин, обрываем прогон, counters.no_proxy_stop=1 и
mark_failed вместо mark_banned — отказ нашей стороны не должен записываться как бан
Циана. Дома и оценки после стопа пропускаем: они идут через тот же пул.

caused_by_no_proxy вынесен в scraper_kit.proxy_errors (у avito #3288 и domclick #3283
живут приватные копии — их схлопывание отдельной правкой).
2026-09-06 02:27:03 +05:00
1200af58f5 fix(tradein): сайдкар отдаёт подтверждённый бан площадки кодом 403, не 500
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
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 / browser-tests (pull_request) Successful in 1m31s
CI Trade-In / backend-tests (pull_request) Successful in 4m58s
Единственный код 500 означал и «площадка забанила», и «сайдкар упал»:
разбор каждого инцидента начинался с ложного следа — в лог провайдера и в
houses.imv_error_reason уезжала httpx-преамбула «Server error '500 Internal
Server Error' for url 'http://tradein-browser:3000/fetch'», то есть текст
ошибки называл гонца, а не виновника.

- browser/server.py: BanPageDetectedError → 403 (доступ ограничен площадкой);
  451 — про юридическую блокировку, это не она. Своих 403 сайдкар не отдаёт
  (400/422/503), код однозначен. classify_browser_probe не задета: у неё любой
  status >= 400 → "sidecar". Тело не меняется — ban_page/status на месте.
- scraper_kit/browser_fetcher.py: текст SidecarBanPageError теперь свой —
  «площадка отдала бан-страницу (upstream 403, ответ сайдкара 403): …».
  Распознавание остаётся по ТЕЛУ и code-agnostic: tradein-browser — отдельный
  образ со своим деплоем, версии штатно расходятся на часы, и гейт по коду в
  этот час уводил бы отказ площадки в инфра-ветку.
- Тесты: 403 → SidecarBanPageError; старый 500 + ban_page → он же; чистый 500
  без ban_page → прежний инфра-диагноз; текст ошибки без «500»/«Server error».

Refs #3288 п.4
2026-09-06 01:58:02 +05:00
e04e315203 fix(scraper-kit): yandex on_bucket отдаёт лоты бакета, а не len(seen)
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 5m3s
`_leaf`/`_degraded` звали on_bucket(bucket_key, len(seen), complete) — int
уезжал в run_yandex_full_load._on_bucket и дальше в save_listings
(`for lot in lots`) → TypeError, ручной full-load Яндекса не сохранял ничего.
Контракт выровнен по cian/avito: провайдер копит лоты бакета (новые в seen)
и отдаёт список. Parity-фикстура подменяла скрапер целиком и слала list по
построению — добавлен прогон run_yandex_full_load через НАСТОЯЩИЙ
YandexRealtyScraper (замокан только gate-JSON транспорт).

Closes #3375
2026-09-06 01:55:45 +05:00
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
2026-09-05 20:22:49 +00:00
1c8035934d Merge pull request 'fix(scraper-kit): domclick city sweep — done_buckets во всех семи выходах, а не только в дрейне и одном бане' (#3372) from fix/3369-domclick-cancel-checkpoint 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 11s
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
2026-09-05 20:19:44 +00:00
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>
2026-09-06 01:16:14 +05:00
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
2026-09-06 01:13:15 +05:00
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
2026-09-06 00:56:36 +05:00
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
2026-09-06 00:51:47 +05:00
769a36098b fix(tradein): считать domklik diff_percent из цен, отвергать не-проценты (#3225)
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 5m6s
offer_price_history.diff_percent у domklik содержал РУБЛИ: загрузчик карточки
клал поле источника priceHistory.diff как есть, а clamp_diff_percent только
зажимал его в ±999999.99 — заведомо неправдоподобное значение проходило молча.
Прод 29.08.2026: 8900 из 11 075 непустых значений с |diff| > 50, p50 = -50 010.

- парсер Домклика считает процент сам из соседних price_rub (сортировка по
  change_time); у самой ранней записи предыдущей цены нет -> NULL, не 0;
- clamp_diff_percent -> validate_diff_percent: |x| > 100 не зажимается, а
  отвергается (NULL + warning с listing_id и сырым значением). Гейт стоит в
  общем хелпере, поэтому закрывает и cian-путь;
- миграция 285 пересчитывает уже собранные domklik-строки оконной lag() по
  (listing_id, change_time); идемпотентна (UPDATE только IS DISTINCT FROM).

Closes #3225
2026-09-06 00:48:08 +05:00
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
2026-09-05 19:28:25 +00:00
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.
2026-09-06 00:21:55 +05:00
6ced16618c Merge pull request 'fix(tradein/yandex): недорендеренная карточка — отказ, не успех: структурный маркер encryptedPhones + порог размера, карточка остаётся в очереди' (#3364) from fix/3191-yandex-detail-completeness 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 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 4m2s
Deploy Trade-In / build-backend (push) Successful in 2m4s
Deploy Trade-In / deploy (push) Has been cancelled
2026-09-05 19:21:06 +00:00
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
2026-09-05 19:19:31 +00:00
78e3da6bc3 fix(tradein): маркер полноты — encryptedPhones ИЛИ redirectPhones (#3191)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
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 5m21s
Ревью #3364. Требуя именно encryptedPhones, отбраковывали бы вечно
необмеренный класс карточек «без телефона / только чат»: в очередь они
возвращаются, а ключ не появится. redirectPhones измерен тем же замером
#3192 и присутствует в обеих ветках (с куками и без).

Текст ABORT: consecutive_none смешанный (фетч-ошибка + parse-None +
недогруз) — «N подряд без обогащения», а не «недогруженных».
2026-09-06 00:14:43 +05:00
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 ВКЛЮЧИТЕЛЬНА» в том же докстринге.
2026-09-06 00:13:11 +05:00
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
2026-09-06 00:02:21 +05:00
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
2026-09-05 23:59:22 +05:00
4b5f8fb54c fix(tradein): недогруженная карточка Яндекса — отказ, а не успех (#3191)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
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 / 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 5m11s
Страница на 1,8 МБ без блока контактов приходит с HTTP 200 и валидным HTML:
window.INITIAL_STATE на месте, parse отрабатывает — и частичная карточка уезжала
в БД с detail_enriched_at, выбывая из очереди навсегда. Единственная проверка
размера (newbuilding.py, len(html) < 500) отвечала на вопрос «пришло ли хоть
что-то»: 1,8 МБ проходит её в 3600 раз.

Признак полноты структурный + размерный, любой из двух даёт отказ:
encryptedPhones (65 вхождений у полных карточек, 0 у недогруза; отдаётся и
анонимной сессии — см. yandex_session.py) и settings.yandex_detail_min_html_bytes
(1 МБ). Наблюдавшийся недогруз ловит именно структурный: 1,8 МБ порог проходит.

В backfill проверка стоит ДО parse: исход incomplete ⊆ failed, save не
вызывается, значит detail_enriched_at не проставляется и следующий снапшот
(detail_enriched_at IS NULL) возьмёт объявление снова. Серия недогрузов двигает
consecutive_none — тот же брейкер, что у parse→None, поэтому вечно недогружаемая
карточка обрывает прогон, а не молотится (per-listing счётчика попыток в схеме
нет).

Фейковые ответы в тестах-соседях (#3196/#3338) теперь при HTTP 200 выглядят
полной страницей — иначе они молча стали бы кейсами про полноту.
2026-09-05 23:59:15 +05:00
35c7ea8492 fix(cian,yandex): skip fully-covered price ranges before probing on resume
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 5m1s
Гейт should_skip живёт в общем движке (walk_price_range, #3315), но предикат
из done-леджера строил и передавал только avito. У cian и yandex та же
бисекция и тот же чекпоинт — на резюме дерево деления спускалось ВНУТРЬ
зачтённых полос живыми probe-запросами: ключи чекпоинта суть границы
ДИНАМИЧЕСКОЙ бисекции, при сдвиге рынка новый лист старому не равен даже
внутри собранной территории, поэтому сравнение строк ничего не ловит.

Формат ключей у провайдеров разный, и ключи не трогаем (иначе протухнут
живые чекпоинты): cian пишет room_label:lo:hi / :open — как avito, парсер
подходит без изменений; yandex пишет _combo_label «rooms:lo-hi» с «None»
вместо открытого потолка, поэтому в done_range_skipper параметризованы
range_sep и open_token (дефолты = прежнее поведение avito/cian).

Один леджер на два режима: у yandex incremental-ключи несут префикс
сегмента (secondary/2:…) и отсев по label их не пропускает, а смешение
режимов блокирует _pick_resume (params IS NOT DISTINCT FROM); у cian
incremental-режима нет вовсе. Записано в докстринге предиката.

Тесты по значению на обоих провайдерах: счётчик стоит на горлышке фетча
(_fetch_page_html / _fetch_page_json), резюм готовой комнатности = 0
запросов, частично покрытая полоса по-прежнему пробивается.

Closes #3359
2026-09-05 23:56:46 +05:00