Commit graph

3451 commits

Author SHA1 Message Date
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
72e3bc9e24 Merge pull request 'feat(tradein/proxy): прогон знает свой узел, а снятый бан перестаёт стирать историю (#3404)' (#3405) from feat/3404-proxy-run-attribution 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 4m14s
Deploy Trade-In / build-backend (push) Successful in 1m39s
Deploy Trade-In / deploy (push) Successful in 1m53s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-09-06 10:18:14 +00: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
3d154f4ec0 Merge pull request 'fix(cian): капча/страница ошибки Циана (HTTP 200) — отказ площадки в сайдкаре и kit, ротация аренды внутри батча (#3402)' (#3403) from fix/cian-captcha-is-platform-refusal 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) Successful in 2m14s
Deploy Trade-In / test (push) Successful in 4m18s
Deploy Trade-In / build-backend (push) Successful in 1m39s
Deploy Trade-In / deploy (push) Successful in 2m1s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-09-06 10:01:30 +00: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
06ad504505 Merge pull request 'refactor(tradein): одна реализация scrape_runs — kit orchestration/runs.py, app-модуль = алиас; counters везде мержатся (#3390)' (#3400) from refactor/3390-single-runs-module into main
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 4m9s
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-backend (push) Successful in 1m42s
Deploy Trade-In / deploy (push) Successful in 7m48s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-09-06 08:11:52 +00:00
ae6ad9e440 docs(#3390): убрать ложные «counters ЗАМЕНЯЕТ» из комментариев/докстрингов; гейт test_3168 — честная формулировка
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 4m51s
2026-09-06 12:34:53 +05:00
bbf38f544f Merge pull request 'fix(tradein/estimator): yandex/cian valuation в /estimate через пул прокси; честный лог при пустом пуле (#3398)' (#3399) from fix/3398-estimator-valuations-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 4m14s
Deploy Trade-In / build-backend (push) Successful in 1m4s
Deploy Trade-In / deploy (push) Successful in 1m33s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
2026-09-06 07:06:47 +00:00
ec7838b7a9 fix(#3398): пустой пул в фоновой догрузке — WARNING без трейсбека (общая функция); комментарий про stateless
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Successful in 4m58s
На проде `ESTIMATE_EXTERNAL_SOURCES_BACKGROUND=true` (docker-compose.prod.yml:296),
поэтому синхронный cian-вызов идёт с `fetch_on_miss=False` и возвращает None ДО
прокси-слоя (`providers/cian/valuation.py:171`) — добавленная в этой ветке ветка
WARNING в `estimate_quality` на проде почти не звучит. Настоящий фетч уходит в
`_defer_external_refresh`, где `NoProxyAvailableError` попадал в общий
`except Exception: logger.exception(...)` → ERROR + traceback → событие в GlitchTip
на каждый /estimate по новому адресу: ровно тот шум, который PR и убирает.

Правка в ОБЩЕЙ функции отложенной догрузки, а не в cian-ветке: через неё идут все
источники фонового режима (yandex тоже — у него swallow живёт внутри
`_get_or_fetch_yandex_valuation_cached`, дыры нет, но следующий источник получит
поведение бесплатно). Для прочих исключений всё как было: `logger.exception`.

Тест по значению: background=True + пустой пул в production → фоновая догрузка cian
логирует WARNING «пул прокси пуст», записей ERROR/traceback у логгера эстиматора нет.
Задача дожидается внутри того же loop'а и не снимая патчей (`_DEFERRED_REFRESH_TASKS`
+ `asyncio.gather`) — иначе `anyio.run` закрывает loop раньше старта задачи и тест
был бы зелёным по построению. На HEAD ветки тест красный:
  ERROR app.services.estimator:estimator.py:910 deferred cian_valuation: догрузка не
  удалась (кэш не прогрет) + Traceback … NoProxyAvailableError.

Комментарий у `_c_kwargs`: весь dict переиспользуется замыканием фоновой задачи, то
есть `config`/`proxy_provider` — один инстанс на два возможно-одновременных вызова.
Корректно ровно пока оба stateless (`RealScraperConfig` — read-only снимок настроек,
`RealProxyProvider` без полей, короткая сессия БД на операцию); появится per-вызов
состояние — фоновой задаче нужен свой инстанс.
2026-09-06 11:58:42 +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
8a5f75d5f4 fix(tradein/estimator): yandex/cian valuation в /estimate через пул прокси, пустой пул не роняет ответ (#3398)
All checks were successful
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 Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m4s
Тот же корень, что у IMV (#3386 / PR #3397): оба вызова шли БЕЗ `proxy_provider`
— `YandexValuationScraper(RealScraperConfig(), delay_provider=...)` и
`estimate_via_cian_valuation(**_c_kwargs)`. `providers/_proxy.py::curl_proxy_url`
считает `use_pool = флаг AND provider is not None`, поэтому пул был выключен по
построению, а curl уходил на env-прокси SCRAPER_PROXY_URL (выключенный узел
#2613): на проде 06.09 каждая проба `/estimate` давала `curl_cffi ProxyError:
CONNECT tunnel failed, response 407`, а в логах это читалось как
«yandex_valuation: empty result» — оба источника мертвы с 02.09.

Kit-стороне правки не нужны: `YandexValuationScraper.__init__` и
`estimate_via_cian_valuation` уже принимают kwarg-only `proxy_provider` и сами
зовут `curl_proxy_url` (yandex — lease на сессию __aenter__/__aexit__, cian —
lease на вызов, release в finally). Провайдер берётся из module-level импорта
`RealProxyProvider` (он stateless: короткая сессия на операцию), поэтому один
инстанс в `_c_kwargs` покрывает и основной вызов, и отложенную фоновую догрузку.

Мягкая деградация сохранена и стала честной в логах: `NoProxyAvailableError`
(проверка по цепочке причин `caused_by_no_proxy`) → WARNING «пул прокси пуст —
продолжаем без Yandex/Cian» и прежний None-путь, а не ERROR «fetch failed» /
«lookup failed» (запрос вообще не уходил — GlitchTip-событие тут было бы шумом).
Строка «yandex_valuation: empty result» переписана: None неразличимо «дом не
найден» и «фетч не дошёл», причина — в строке scraper_kit выше.

Тесты (7, по значению; на main все красные): оба call site получают провайдера;
пустой пул в production → оценка без этих источников, без исключения, HTTP не
уходит, в логе «пул прокси пуст»; lease освобождён ровно один раз на успехе и на
ошибке фетча — через настоящий `curl_proxy_url`, а не мок провайдера.
2026-09-06 11:30:28 +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
fd4bd65126 Merge pull request 'fix(tradein): прогоны, оборванные деплоем (interrupted=1), невидимы лестницам стриков и honest-status-гейтам (#3393)' (#3395) from fix/3393-interrupted-runs-outside-streaks into main
Some checks failed
Deploy Trade-In / changes (push) Has been cancelled
Deploy Trade-In / build-backend (push) Has been cancelled
Deploy Trade-In / build-frontend (push) Has been cancelled
Deploy Trade-In / build-browser (push) Has been cancelled
Deploy Trade-In / deploy (push) Has been cancelled
Deploy Trade-In / deploy-status (push) Has been cancelled
Deploy Trade-In / test (push) Has been cancelled
Deploy Trade-In / perimeter-smoke (push) Has been cancelled
2026-09-06 05:53:24 +00:00
6f97995140 fix(tradein/estimator): IMV-путь берёт прокси из пула, пустой пул не ломает /estimate (#3386)
All checks were successful
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 / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Successful in 4m55s
Оба вызова `evaluate_via_imv` в `_get_or_fetch_imv_cached` шли без
`proxy_provider`, а `providers/_proxy.py::curl_proxy_url` считает
`use_pool = флаг AND provider is not None` — пул был выключен по построению,
curl-сессия уходила на env-прокси SCRAPER_PROXY_URL (мёртвый узел, #2613).

Провайдер берётся из уже существующего module-level импорта
`app.services.scraper_adapters` (в estimator цикла нет, в отличие от
house_imv_backfill — там lazy import вынужденный). Lease — один на вызов IMV,
acquire/release внутри `curl_proxy_url`, release в finally на всех выходах.

Пустой пул в проде (`NoProxyAvailableError`, в т.ч. завёрнутый — проверка по
цепочке причин `caused_by_no_proxy`) остаётся graceful: `_get_or_fetch_imv_cached`
возвращает None, ответ отдаётся без IMV-якоря. Причина в логе теперь честная —
«пул прокси пуст», а не «fetch failed» (запрос не уходил вовсе).
2026-09-06 10:32:42 +05: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
aeb1b2b3b9 Merge pull request 'fix(scraper-kit/scheduler): SIGTERM-drain помечает in-flight прогоны interrupted=1 при hard-cancel; честный лог drain'а (#3391)' (#3392) from fix/drain-marks-inflight-app-tasks 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 4m5s
Deploy Trade-In / build-backend (push) Successful in 1m38s
Deploy Trade-In / deploy (push) Successful in 2m7s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
2026-09-06 04:14:06 +00: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
3fe2d310b2 Merge pull request 'fix(tradein/yandex): newbuilding resolve/fetch_jk через пул прокси + стоп на пустом пуле (#3197, точки 2-3)' (#3389) from fix/3197-yandex-newbuilding-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 4m5s
Deploy Trade-In / build-backend (push) Successful in 1m38s
Deploy Trade-In / deploy (push) Successful in 7m53s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
2026-09-06 02:24:44 +00:00
6444ff7e37 Merge pull request 'fix(tradein): пустой пул на старте батча пишет no_proxy_stop у cian/domclick/avito detail-бэкфиллов (#3384)' (#3388) from fix/3384-no-proxy-at-batch-start into main
Some checks failed
Deploy Trade-In / test (push) Blocked by required conditions
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (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) Has been cancelled
2026-09-06 02:24:38 +00:00
47dabec678 fix(#3384): cian — в mark_failed уходит только измеренное, нули heartbeat не затираются; стаб пула как прод
All checks were successful
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 Trade-In / 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 5m0s
Ревью нашло у цианa (в отличие от avito/домклика с живым counters.to_dict()) старый
словарь в общем except: реальные значения присваиваются уже ПОСЛЕ возврата из
backfill_cian_history, а отказ бывает и посреди неё — пул опустел между стадиями, упал
SELECT домов. Тогда поверх измеренного в запись прогона уезжали нули, и SQL-разбор
простоя (#3288/#3367) читал «к площадке не ходили» про прогон, который ходил.

Механизм оказался хуже описанного в ревью: mark_failed мержит counters (`counters ||
:counters`) только в kit-копии, а cian/avito/домклик зовут app.services.scrape_runs, где
UPDATE counters ЗАМЕНЯЕТ (scrape_runs.py:738). Поэтому «отдать только {no_proxy_stop: 1}»
стёрло бы измеренное начисто; вместо этого _heartbeat кладёт свой снимок в те же
counters (nonlocal), и в mark_failed уезжает последнее измеренное + флаг.

Тест по значению: heartbeat записал listings_processed=5, дальше пул пуст → в jsonb-
payload mark_failed должно остаться 5, а не 0 (проверяется сам payload UPDATE'а,
runs_mod настоящий). На HEAD ветки красный: `counters={'listings_processed': 0, ...,
'no_proxy_stop': 1}: нули поверх измеренных 5`.

Стаб пула приведён к проду: RealProxyProvider.acquire при пустом пуле ВОЗВРАЩАЕТ None
(scraper_adapters.py:230), а не поднимает, — исключение из провайдера глотал
`except Exception` в _acquire_lease и приходило к тому же отказу другим путём. Теперь
NoProxyAvailableError рождается там же, где в проде (browser_fetcher.py:712, ветка
`lease is None and use_pool and production`) — проверено прогоном против до-#3384
исходников: все три теста красные, трейс из _acquire_lease.

_prod_pool патчит app.core.config.settings явно + assert, что все три задачи держат тот
же синглтон: раньше патч через chb.settings выглядел настройкой одного циана.
2026-09-06 06:27:45 +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
caa3b8b056 fix(tradein): пустой пул ДО первого объявления теряет диагноз в записи прогона (#3384)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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
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
Lease берётся один раз в BrowserFetcher.__aenter__, поэтому на проде с пустым пулом
NoProxyAvailableError вылетает из самого `async with` — ДО первой карточки и мимо
стоп-механики внутри цикла (no_proxy_stop = True; break), которая и пишет
counters.no_proxy_stop. Общий `except Exception` ловил его и делал mark_failed с
нулевыми counters без ключа: прогон, который к площадке не ходил вообще, в SQL-разборе
простоя по counters.no_proxy_stop (#3288/#3367) не находится.

Дыра одинаковая у всех трёх бэкфиллов (avito её тоже не обрабатывал: __aenter__
вызывается напрямую строкой 427, отказ уходит в тот же общий except). Правка — в трёх
уже существующих обработчиках, которые и так зовут mark_failed: ключ no_proxy_stop=1
при caused_by_no_proxy(exc). Оборачивать `async with` в try/except пришлось бы с
переносом ~200 строк тела под новый отступ в каждом файле, и покрывало бы только
падение на входе; здесь ловится любой путь мимо цикла.

Тест — через настоящий BrowserFetcher: подделан только провайдер прокси (его acquire
поднимает NoProxyAvailableError), отказ рождается там же, где в проде. Проверяется
failed + no_proxy_stop=1 + attempted=0 (у циана listings_processed=0) и ноль POST'ов
в сайдкар.

Closes #3384
2026-09-06 05:48:32 +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
dc63b1830b docs(#3376): ссылка на follow-up #3385 вместо TODO в yandex_price_history
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 4m52s
2026-09-05 22:57:04 +00: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
6875992746 Merge pull request 'fix(tradein/compose): USE_PROXY_POOL_CURL/BROWSER и сервису backend — не только scraper (#3386)' (#3387) from fix/backend-proxy-pool-flags into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Successful in 36s
Deploy Trade-In / build-frontend (push) Successful in 2m8s
Deploy Trade-In / test (push) Successful in 4m3s
Deploy Trade-In / build-backend (push) Successful in 31s
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
2026-09-05 22:30:40 +00:00
b65f75c1fc fix(tradein/compose): USE_PROXY_POOL_* и для backend, не только scraper
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
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 / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
`USE_PROXY_POOL_CURL`/`USE_PROXY_POOL_BROWSER` объявлены только у сервиса
scraper (#2126/#2160). В процессе backend оба флага оставались False, из-за
чего `_kit_proxy_provider()` возвращал None, а `curl_proxy_url` /
`BrowserFetcher._acquire_lease` игнорировали переданный proxy_provider и
уходили на env-фолбэк `SCRAPER_PROXY_URL` — на проде это выключенный узел
(407) → camoufox InvalidIP, 83 провала запуска сайдкара за 3 ч 05.09.

Правка — только объявление тех же двух переменных в `environment:` backend.
`environment:` перекрывает `env_file` (backend/.env.runtime), значений
USE_PROXY_POOL_* в runtime-env нет, так что конфликта нет.

Гейты: yaml.safe_load rc=0, scripts/check-compose-ambiguous-hosts.py rc=0.

Refs: issue «tradein-backend без USE_PROXY_POOL_* → браузерные скрейпы на мёртвый env-прокси»

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 03:16:26 +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
2426b529a8 Merge pull request 'fix(browser-sidecar): бан-страница площадки отдаётся 403, а не 500 (#3288 п.C)' (#3379) from fix/3288-sidecar-ban-status-code into main
Some checks failed
Deploy Trade-In / test (push) Blocked by required conditions
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (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) Has been cancelled
2026-09-05 21:46:28 +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
235cc3065e test(#3375): стаб доходит до _degraded и многостраничного листа; комментарий про красноту
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 5m9s
2026-09-06 02:38:12 +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
8a969c06c2 Merge pull request 'fix(deploy-metrics): пересоздавать alloy после выката конфига + гейт по иноду (#3380)' (#3381) from fix/3380-deploy-metrics-alloy-recreate into main
All checks were successful
Deploy Metrics / server (push) Successful in 14s
Deploy Metrics / agent-apps (push) Successful in 20s
Deploy Metrics / agent-infra (push) Successful in 19s
2026-09-05 21:17:52 +00:00
c9a9085df6 fix(deploy-metrics): пересоздавать alloy после выката конфига, гейт по иноду
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
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
Конфиг Alloy смонтирован бинд-маунтом ОДНОГО файла, а `git reset --hard`
в деплое не правит его на месте, а пишет новым инодом. `up -d` сравнивает
описание сервиса, содержимое бинд-маунта в сравнение не входит — контейнер
не пересоздаётся и продолжает читать прежний, уже удалённый инод.

Отказ беззвучный: на диске новый конфиг, деплой зелёный, а фильтрация идёт
по старому. Пойман на проде 06.09: на Beget после success-деплоя контейнер
держал инод от 26.08, и новый фильтр заработал только после ручного
`docker restart gendesign-alloy`.

Тот же приём, что у alertmanager в джобе server. Позиции чтения журнала
лежат в томе alloy_data и пересоздание переживают.

Гейт после подъёма сверяет инод файла на хосте с инодом внутри контейнера:
расхождение = деплой красный, а не «зелёный со старым конфигом».
2026-09-06 02:11:38 +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