fix(tradein/proxy): один прокси на сессию браузера вместо смены на каждом запросе #2640
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#2640
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-sticky-proxy-per-session"
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
Проблема (живая регрессия, срочный фикс).
BrowserFetcher._pool_proxy()делалacquire()+release()на КАЖДЫЙ/fetch. Пока пул был из 1 узла, это не было заметно (acquire всегда выдавал тот же прокси). После восстановления пула до 3 узлов (id 9/10/11,affinity=any)acquire()(ORDER BY last_ok_at NULLS LAST, id) стал гарантированно выдавать ДРУГОЙ прокси на каждый/fetch.tradein-browserрелончит camoufox при любой смене желаемого прокси (server.py::_ensure_browser) — по прод-логам ~17 relaunch'ей за 15 минут, каждый 5-10с впустую + потеря сессионных cookie (усиливает антибот-подозрительность: поток «первых визитов» с пустой cookie-jar).Фикс.
BrowserFetcherживёт столько же, сколько прогон (создаётся в__aenter__скрапера/pipeline, закрывается в__aexit__— часто через целый city-sweep со множеством anchor'ов/detail-фетчей). Lease теперь берётся ОДИН раз в__aenter__и хранится вself._leaseна весь жизненный цикл; отпускается один раз в__aexit__(вfinally— не течёт при исключении, включая исключение внутриasync with-блока пользователя).Коллизия с
reap_stale_leases(главный риск, разобран явно)STALE_LEASE_MINUTES=30, а прогоны бывают дольше (full-load Циана шёл часами). Держать lease весь прогон без изменений означало бы, что reaper отберёт его на середине прогона (следующий tickproxy_healthcheck, каденс 30 мин).Решение — heartbeat, не увеличение порога. Не трогали
STALE_LEASE_MINUTES— это ослабило бы реальную задачу reaper'а (освобождать lease мёртвого/зависшего run'а, который никогда не позовётrelease). Вместо этого добавленproxy_pool.touch(db, proxy_id)— лёгкийUPDATE ... SET leased_at = now() WHERE leased_by IS NOT NULL(no-op если уже не арендован).BrowserFetcherзовётtouch()на каждый/fetch(успешный и неуспешный — сам факт завершённого запроса доказывает, что процесс жив) через новый методProxyProvider.touch()(Protocol вcontracts.py+ реализация вRealProxyProvider). Пока идёт трафик —leased_atпостоянно свежий, reaper не трогает. Упавший/зависший run (ни одного/fetchдольше 30 мин) по-прежнему реапится штатно — semantics crash-recovery не ослаблена, просто измеряется от «последней активности», а не от «момента acquire». Тестыtest_touch_prevents_reap_of_long_running_session/test_touch_absence_still_reaps_dead_sessionвtests/services/test_proxy_pool.pyпроверяют оба плеча явно.Остальное поведение
mark_health(ok)вызывается на каждый/fetch(не только по итогу сессии) — сохранена прежняя грануляция:DISABLE_THRESHOLDпула считаетconsecutive_failsпо попыткам, огрубление до «одна оценка на сессию» замаскировало бы деградирующий узел._LEASE_ROTATE_AFTER_FAILS=3подряд неудачных/fetchlease ОСОЗНАННО меняется один раз (releaseстарого +acquireнового), счётчик обнуляется успехом. Следующий/fetchнесёт новый proxy-url → camoufox релончится РОВНО один раз (не мечемся на каждый запрос, как раньше).run_idНЕ прокидывали вacquire()(в логахby=-1/NON_RUN_LEASE_MARKER) — это только log-attribution, не требуется для фикса; плумбинг потребовал бы трогать 20+ call-site'овBrowserFetcher(...)(pipeline.py,tasks/*.py) ради косметики.Curl-путь (
providers/_proxy.py::curl_proxy_url) — НЕ тронут, той же проблемой не страдаетПроверено ПЕРЕД правкой: curl_cffi
AsyncSessionтам уже создаётся per-request (нет персистентного процесса-браузера). Смена прокси между запросами не вызывает cost релонча — там нет процесса, который нужно перезапускать. Потеря session/cookie-continuity там та же, что и до введения proxy-пула (каждый curl_cffi-вызов и раньше создавал свежую сессию) — proxy-пул это свойство не ухудшил.Прод-замер (read-only,
ssh gendesign,docker logs --since 1h)tradein-scraper: 255leased proxy-событий за последний час (пары acquire/release почти на каждый/fetch).tradein-browser: 35 срабатываний"прокси изменился ... — relaunch перед fetch"за тот же час (~1 relaunch каждые ~1.7 мин) — тот же класс регрессии, что и заявленные ~17/15мин (разное окно замера, тот же порядок величины).BrowserFetcher, обычно = один scrape-run/city-sweep с множеством anchor'ов) + опционально 1 раз при осознанной ротации на 3 подряд провалах — ожидаемое снижение на 90%+ по количеству relaunch'ей.Тесты
Все новые тесты falsified: сохранена сигнатура, обнулено поведение под
git stashsource-файлов → 15 failed / 35 passed на pre-fix коде (11AssertionError— genuine value-based падения ключевого регресса:test_two_sequential_fetches_use_same_proxy,test_acquire_happens_once_in_aenter_not_per_fetch,test_lease_released_on_session_close/..._on_exception_inside_session, ротация после N провалов, mark_health/touch грануляция и т.д.; 4AttributeError—touch()/rotate-constant реально не существовали до фикса, не что-то замаскированное). На фиксе: 50/50 passed.Полный
pytestвtradein-mvp/backend: 3219 passed, 1 failed (pre-existingtest_search_api.py::test_search_cache_hit, 401 RBAC, не наш скоуп — упомянут в задаче), 9 skipped.ruff checkчист на всех изменённых файлах (app/services/proxy_pool.py,app/services/scraper_adapters.py,packages/scraper-kit/src/scraper_kit/browser_fetcher.py,packages/scraper-kit/src/scraper_kit/contracts.py, соответствующие тесты).Test plan
pytest tests/test_kit_browser_fetcher_proxy_pool.py tests/services/test_proxy_pool.py tests/test_scraper_adapters_contracts.py— 50/50 greenpytest tradein-mvp/backend— 3219 passed, 1 pre-existing fail, 9 skippedruff checkчистtradein-scraper/tradein-browser— частотаleased proxy/прокси изменилсядолжна упасть на порядокRefs #2164
c6590c5ae9to5e247c6264done#2698