fix(tradein): пустой пул на старте батча пишет no_proxy_stop у cian/domclick/avito detail-бэкфиллов (#3384) #3388

Merged
bot-backend merged 2 commits from fix/3384-no-proxy-at-batch-start into main 2026-09-06 02:24:38 +00:00

2 commits

Author SHA1 Message Date
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
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