fix(tradein): пустой пул на старте батча пишет no_proxy_stop у cian/domclick/avito detail-бэкфиллов (#3384) #3388
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3388
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/3384-no-proxy-at-batch-start"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #3384.
Дефект.
BrowserFetcher._acquire_lease()живёт в__aenter__; при пустом пуле до первого объявленияNoProxyAvailableErrorвылетает изasync with BrowserFetcher(...)(cian:218, domclick:403) и из прямогоawait browser_fetcher.__aenter__()у avito (avito_detail_backfill.py:427) — мимо стоп-механики внутри цикла. Общиеexcept Exceptionписалиmark_failedсо стухшими нулевыми counters безno_proxy_stop→ SQL-разбор простоя поcounters.no_proxy_stop(#3288/#3367) такие прогоны не находил. У avito дыра та же — стоп #3288 живёт внутри цикла, до которого дело не доходит.Фикс (вариант B, три правки по ~6 строк в точках, которые и так зовут
mark_failed):app/services/scheduler.py:209-218(cian-исполнитель) —caused_by_no_proxy(exc)→counters["no_proxy_stop"] = 1передmark_failed;app/tasks/domclick_detail_backfill.py:645-654— то же через уже живущий_caused_by_empty_pool;app/tasks/avito_detail_backfill.py:1091-1100— то же.Вариант A (
try/exceptвокругasync with) потребовал бы переиндентировать сотни строк в трёх файлах и ранний return с ручной финализацией у avito; B ловит ещё и любой путь мимо цикла (ре-acquire при ротации, блок домов у cian).Тест
tests/test_3384_no_proxy_at_batch_start.py(3 теста): подделка одна — провайдер, чейacquire()поднимаетNoProxyAvailableError;BrowserFetcherнастоящий;httpx.AsyncClient.post—assert_not_called;mark_done/mark_banned/mark_backfill_finishedне вызваны;status='failed',no_proxy_stop=1,attempted=0.Фальсификация (источники откачены, тест на месте):
KeyError: 'no_proxy_stop'× 3 — при этом в логеBrowserFetcher: proxy_pool acquire(avito) empty in production — refusing (no HTTP request)и трасса__aenter__ → _acquire_lease → raise NoProxyAvailableError— отказ родился в настоящем фетчере,mark_failedвызван без диагноза. Восстановлено — зелёное.Прогоны: полный backend
5512 passed, 35 skipped(rc=0); ruff OK.Приёмка на проде (событийная): прогон любого из трёх источников при пустом пуле на старте —
status='failed',counters.no_proxy_stop=1,attempted=0(cian:listings_processed=0), в логахproxy_pool acquire(<source>) empty in production — refusingбез POST в сайдкар; попадает в выборку разбора простоя поno_proxy_stop.Code-review (07.09): ✅ — трассировка всех трёх
exceptпо коду (промежуточных перехватчиков нет; у avitoown_browser=Trueставится ПОСЛЕ__aenter__,BrowserFetcher.__aenter__сам закрывает клиент при отказе — течь нечему); контракты #3196/#2764/#3367 не задеты (гейт типовой по цепочке причин);no_proxy_stopне входит в_RESULT_COUNTER_KEYS→ свежесть наfailedне обновляется; тест не тавтологичен — исключение рождает боевойbrowser_fetcher.py:712.Докатываю до мержа один реальный минор: у cian
countersв этомexcept— предынициализированный нулевой словарь, аmark_failedмержит через jsonb||→ пул, опустевший на стадии домов, затёр бы нулями уже записанное heartbeat'ом (и SQL-разбор прочитал бы «к площадке не ходили» про прогон, который ходил). Вmark_failedиз этой ветки уходит только флаг. Плюс стаб провайдера приводится к проду (acquire → None, отказ рождается в_acquire_lease).Ревью нашло у циан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 выглядел настройкой одного циана.Доработка (
47dabec6): рецепт ревью «отдать вmark_failedтолько флаг» на этом пути потерял бы данные —scheduler.py:50импортируетapp.services.scrape_runs, гдеmark_failedcounters ЗАМЕНЯЕТ (scrape_runs.py:738), мерж||только в kit-копии (scraper_kit/orchestration/runs.py). Вывод ревью (нули не должны ложиться поверх замера) верен, обоснование — нет; проверено по импорту. Сделано:_heartbeatдержит последний снимок в тех жеcounters, вmark_failedуезжает измеренное +no_proxy_stop=1. Тест краснел на HEADassert 0 == 5(нули поверх измеренных 5). Стаб провайдера приведён к проду (acquire → None, отказ рождается в_acquire_lease:712, в логе прод-warningproxy_pool acquire(cian) empty in production — refusing). Две копии одной функции с противоположной семантикой — #3390.