fix(tradein/scraper): подключить браузерный путь Авито к пулу прокси #2637
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2637
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-avito-browser-proxy-pool"
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?
Summary
Root cause issue #2613/#2616 (52/53
banned-прогонов за 14 дней): Циан/Яндекс передаютproxy_providerв браузерныйBrowserFetcher(build_browser_fetcher(config, source, proxy_provider=...)),Авито — нет.
AvitoScraper.__init__не принималproxy_providerвообще, поэтому браузерныйпуть Avito SERP всегда шёл через мёртвый env-прокси
BROWSER_PROXY_AVITO(407 auth failure,подписка mobileproxy закрыта с начала июля) вместо пула
scrape_proxies.Прошито ровно по образцу Cian (
CianScraper.__init__(..., proxy_provider=...)→build_browser_fetcher(config, "cian", proxy_provider=self._proxy_provider)):AvitoScraper.__init__— новый keyword-onlyproxy_provider: ProxyProvider | None = None.__aenter__—build_browser_fetcher(self._config, "avito", proxy_provider=self._proxy_provider).AvitoScraper(...)вorchestration/pipeline.py+orchestration/scheduler.py+backend/app/api/v1/admin.pyпроверены индивидуально (см.таблицу ниже) — не единообразный паттерн, потому что у Avito ДВЕ разные архитектуры вызова.
Таблица: какие места прошиты, какие — нет и почему
Ключевое наблюдение:
AvitoScraperконструируется в pipeline.py двумя разными паттернами.Паттерн A (shared-browser, ручной override) —
run_avito_pipeline,run_avito_city_sweep,run_avito_newbuilding_sweep: одинBrowserFetcherстроится ОДИН раз на весь sweep (не накаждый anchor — экономия ресурсов), сразу с
proxy_provider=proxy_provider, и присваиваетсяscraper._browser = shared_bfнапрямую —AvitoScraper.__aenter__()для этих scraper'оввообще не вызывается. Пул там уже был подключён ДО этого PR (это НЕ баг).
Паттерн B (
async with AvitoScraper(...) as scraper) —run_avito_full_loadиbackend/app/api/v1/admin.py's/scrape:__aenter__()реально исполняется →build_browser_fetcherреально вызывается с тем, что передано в конструктор. Здесьproxy_providerload-bearing — раньше отсутствовал совсем.pipeline.py::run_avito_pipeline(~L555)AvitoScraper(config, proxy_provider=proxy_provider)— не load-bearing, добавлен для консистентности/defense-in-depthpipeline.py::run_avito_city_sweep(~L1112)pipeline.py::run_avito_newbuilding_sweep(~L1754)pipeline.py::run_avito_full_load(~L3469,async with)__aenter__)proxy_providerв сигнатуру функции +AvitoScraper(config, proxy_provider=proxy_provider)scheduler.py::_job_avito_full_load/_job_avito_full_load_exhaustivectx.proxy_providerне пробрасывался вrun_avito_full_load(...), хотя_job_cian_full_loadуже пробрасывалproxy_provider=ctx.proxy_providerв оба вызоваbackend/app/api/v1/admin.py::/scrape(source=avito)__aenter__)proxy_providerуже вычислен в scope (_kit_proxy_provider()) и передавался в Cian/Yandex на соседних строках, но не в AvitoAvitoScraper(config, delay_provider=get_scraper_delay, proxy_provider=proxy_provider)backend/app/tasks/avito_detail_backfill.py(detail-enrichment, НЕ SERP)BrowserFetcher(source="avito", endpoint=...)безproxy_provider/use_poolвообщеproxy_providerчерезrun_avito_detail_backfill+ доп. параметр, вне границ этой задачи. Флагирую как follow-up.Оценка эффекта (read-only,
scrape_runs, последние 14 дней, прод)52
banned-прогона за 14 дней — все Avito.cian_*/yandex_*/domclick_*= 0 bannedза тот же период. Разбивка по error-тексту: 51/52 =
"...browser unavailable (proxy may be down)"(сидекар получил 503 от tradein-browser — Camoufox не смог поднять браузер черездохлый прокси), 1/52 = реальный firewall-детект (
avito_city_sweep_serov#2964,"Avito SERP firewall (browser-mode) — IP banned"). (Задача цитирует 52+1=53 — у меня ровно14×24ч от текущего момента дало 52 total; расхождение на 1 прогон — вероятно граница
временного окна, не противоречит выводу: подавляющее большинство banned = мёртвый прокси, не
реальный бан площадкой.)
run_avito_full_load (13 banned) и run_avito_newbuilding_sweep/run_avito_city_sweep*
(12+12+1=25 banned, паттерн A — пул уже был подключён через shared BrowserFetcher) —
все они ловили ОДИН и тот же сбой:
browser unavailable (proxy may be down). Для паттерна Aэто означает, что пул
scrape_proxiesтоже не спасал в моменты этих прогонов (пул on,но узел не достался — см. предупреждение о конкуренции ниже) — иначе они бы не банились.
Для
run_avito_full_load(паттерн B, 13 прогонов) причина прямая и однозначная: пул не былподключён вовсе, поэтому 100% фетчей шли на дохлый env-прокси.
После этого PR, при условии живого узла в пуле в момент фетча:
run_avito_full_loadполучит рабочий прокси всегда (единственное исправление, актуальное для НЕЕ), а
city_sweep/newbuilding_sweep получат дополнительный шанс (они и так были подключены к пулу
через паттерн A) — реальный прирост зависит от текущей загрузки пула (см. ниже).
Конкуренция за узлы пула — предупреждение
Прод (
scrape_proxies, только что проверено read-only):Живых узлов 2 из 4, из которых реально доступен ОБЩЕМУ пулу avito/cian/yandex ровно ОДИН
(
id=10, "any"-affinity;id=1зарезервирован под domclick и не попадает в fallback, пока уdomclick-affinity нет второго живого узла —
acquire()'s EXISTS-guard). Это значит:(curl ИЛИ browser, avito ИЛИ cian ИЛИ yandex). Все три sweep-семейства теперь (после этого
PR) реально претендуют на этот единственный узел — раньше Avito browser-путь на пул не
претендовал вовсе (шёл мимо), значит после мержа конкуренция за id=10 вырастет.
_pool_proxy()(
browser_fetcher.py) сегодня (до PR #2634) молча fallback на env (yield None, None→browser использует
BROWSER_PROXY_AVITO) — который мёртв. Т.е. в моменты пиковойконкуренции Avito по-прежнему может банится тем же образом, что сейчас, просто РЕЖЕ
(не на каждом anchor'е/бакете, а только когда узел занят).
fix/tradein-no-dead-env-proxy-fallback, open, mergeable, база = тот жеmain@964b62d, файлы НЕ пересекаются с этим PR — конфликтов нет) добавляетNoProxyAvailableErrorвместо тихого env-fallback в prod при пустом пуле — но его текстявно указывает, что этот класс ошибок пока НЕ отделён от
is_soft_banвavito/serp.py:535-540(любойstatus==503/"browser unavailable"безусловно = soft-ban →после исчерпания rotation-бюджета →
mark_banned). Значит даже после обоих PR — приреальном исчерпании пула Avito всё ещё уйдёт в
banned, просто по другой причине(
NoProxyAvailableErrorвместо мёртвого env-прокси). Это осознанно не чиню здесь (out ofscope, по границам задачи и по тексту самого PR #2634, который называет это отдельным
следующим шагом).
use_proxy_pool_browser=trueуже включённа прод (
tradein-scraper, docker-compose.prod.yml:217) — то есть фикс реально заработаетсразу после деплоя. Но с 1 живым "any"-узлом на троих источников empирический эффект будет
ЧАСТИЧНЫМ, пока пул не пополнят (issue за пределами этой задачи — оператору нужно
реанимировать/добавить прокси-узлы).
Curl-путь Avito (шаг 4 диагностики)
AvitoScraper._build_cffi_session(curl_cffi fallback внутри browser-mode + весьSCRAPER_FETCH_MODE=curl_cffilegacy-путь) используетself._config.scraper_proxy_url(env, mobile backconnect-прокси
mproxy.site/ard.mobileproxy.space) — НЕProxyProvider/пул
scrape_proxies. Это намеренно другой, отдельный механизм (backconnect сself-rotation через
avito_proxy_rotate_url/changeip), а не асимметрия с браузерным путём:IMV-оценка Avito (
avito/imv.py) уже пул-aware черезcurl_proxy_url()(#2163) за флагомuse_proxy_pool_curl— а вот SERP-curl-фоллбэк (_build_cffi_session) — нет, и остаётсятаким же, каким был. Асимметрия, которую чинит этот PR, — только браузерный путь
(единственный, где реально сидел мёртвый прокси и который реально генерил все 52 banned).
Curl-путь НЕ трогал.
Falsification
Для каждого нового теста подтверждено: подмена реализации (сигнатура/kwarg остаётся, но
proxy_providerне доходит доbuild_browser_fetcher/AvitoScraper/run_avito_full_load)даёт
AssertionErrorна значении (assert None is <sentinel>), НЕTypeError— записанопострочно для каждого из 8 новых/дополненных сценариев (constructor→
__aenter__×3,run_avito_full_load×2, scheduler_job_avito_full_load(_exhaustive)×2, admin/scrape×2,run_avito_city_sweep/run_avito_newbuilding_sweepconstructor-consistency ×2 — итого выполнено7 ручных falsification-прогонов на затрагивающих код-путях, каждый упал на значении, затем
код возвращён к исходному состоянию).
Test plan
cd tradein-mvp/backend && uv run pytest -q— 3189 passed, 9 skipped, 1 knownpre-existing fail (
tests/test_search_api.py::test_search_cache_hit, 401 RBAC — не вscope этой задачи).
ruff check(selectE,F,I,B,UP,N,RUF, ignoreRUF001-3) — чисто на всех изменённыхфайлах (2 pre-existing E501 в
avito/serp.pyдалеко от диффа, не мои строки).scrape_runs(banned breakdown за 14д),scrape_proxies(live-nodes),docker exec tradein-scraper printenv | grep PROXY_POOL(подтвержденоUSE_PROXY_POOL_BROWSER=trueуже на sched-контейнере — фикс сработает без доп. deploy-шагов).fix/tradein-no-dead-env-proxy-fallback, issue #2616) НЕ пересекаютсяпо файлам — проверено (
git diff --statобеих веток, оба отmain@964b62d).Файлы
tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/avito/serp.py— конструктор +__aenter__tradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/pipeline.py— 4 места созданияAvitoScraper+ сигнатураrun_avito_full_loadtradein-mvp/packages/scraper-kit/src/scraper_kit/orchestration/scheduler.py— 2 вызоваrun_avito_full_loadtradein-mvp/backend/app/api/v1/admin.py—/scrapeendpointtradein-mvp/backend/tests/test_kit_serp_proxy_pool.py,test_scraper_kit_pipeline_parity.py,test_scraper_kit_pipeline_parity2.py,test_scraper_kit_scheduler_parity.py,test_admin_scrape_avito_proxy_provider.py(новый)Refs #2613, #2616
done#2698