fix(tradein/proxy): операции пула прокси уходят с event loop публичного /estimate — acurl_proxy_url через to_thread (#3398 п.1) #3407
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#3407
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/3398-pool-ops-off-event-loop"
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?
Часть #3398 (п. 1). Не закрывает issue.
Дефект. На
/estimate(1 воркер uvicorn, #3083) вход вcurl_proxy_urlсинхронный и до первогоawait(IMVimv.py:533, yandexvaluation.py:200__aenter__, cianvaluation.py:196):acquire/mark_health/mark_banned/releaseчерезRealProxyProviderходят в БД на event loop — 3 источника на cache-miss;_with_budgetне может прервать синхронный вход; при исчерпанном пуле коннектов checkout ждёт до 30 с, держа loop для всех запросов.Фикс (
scraper_kit/providers/_proxy.py): async-обёрткаacurl_proxy_urlнад тем же синхроннымcurl_proxy_url(он не тронут):__enter__/__exit__черезasyncio.to_thread; выход тоже в потоке (иначеmark_health/releaseдержали бы loop так же);except BaseException+cm.__exit__(type, exc, tb)сохраняет #3397 (отмена →ok=False→mark_health(False)+release). Новое по построению: отмена может прийти ВО ВРЕМЯacquire(раньше невозможно) — поток всё равно выдаст аренду, поэтому вход подasyncio.shield, а вexceptдожидаемся его и делаем__exit__в потоке, иначеwait_for-таймаут при исчерпанном пуле оставлял бы lease доreap_stale_leases.Переключены три async-сайта
/estimate:avito/imv.py::evaluate_via_imv(AsyncExitStack),yandex/valuation.py::YandexValuationScraper.__aenter__/__aexit__,cian/valuation.py::estimate_via_cian_valuation(async with). Оставлены на sync:cian/detail.py::fetch_detail,cian/newbuilding.py::resolve_cian_zhk_url_via_search— async, но под scheduler'ом вtradein-scraper(loop не публичный); прямых вызовов изbackend/appнет.Thread-safety:
RealProxyProviderstateless,SessionLocal()на операцию;to_threadкопируетcontextvars→current_run_idиз #3405 виден вacquire;NoProxyAvailableError/ProxyBanErrorпробрасываются тем же типом (caused_by_no_proxyработает).Тесты
tests/test_3398_pool_ops_off_event_loop.py(6, через настоящийcurl_proxy_urlи настоящийYandexValuationScraper.__aenter__; провайдер-дублёрtime.sleep(0.3)): loop не блокируется во времяacquire(соседняя корутина тикает >100 раз); lease ровно один раз на успехе / ошибке / таймауте внутри тела / отмене во времяacquire;NoProxyAvailableErrorпереживаетto_thread.Фальсификация (
git apply -Rkit-файлов):loop простоял всё время acquire: тиков всего 0иDID NOT RAISE TimeoutError— 2 failed по значению; четыре release-теста зелёные и на main (регресс-гейт обёртки).Прогоны: полный backend
5617 passed, 35 skipped(rc=0); ruff OK.Приёмка на проде: p95
/estimateна cache-miss не растёт; те жеleased/released proxyстроки (3 на запрос), без ростаreap_stale_leases; атрибуция узла (#3404) у прогонов scheduler'а проставляется.Deep-ревью (06.09): ⚠️ minor. Исполнением: отмена во время
acquire→TimeoutError, release ровно один раз; гипотеза проRuntimeErrorневерна — генератор безraiseдал бы__exit__ → Trueи ТИХОЕ подавление отмены (raise на месте, контракт цел); двойной__exit__невозможен и безвреден;current_run_id(#3405) виден в потоке ([4242]); executor 16 потоков при спросе ≤12;AsyncExitStackкорректен.Докатываю до мержа: M1 — тест отмены-на-входе зелёный и без ветки
except BaseException(аренду возвращал рефкаунт генератора приshutdown_default_executorДОassert) → срезreleasedвнутри корутины (мутация:[] != [7]); M2 — при второй отмене во времяawait enter__exit__не вызывается →while not enter.done(): await shield(enter); M3 — потолок бюджета в докстринге (ждём поток целиком). Follow-up #3408: пул коннектов 15 против пикового спроса до 28 (пул-операции больше не сериализуются loop'ом), syncdb.executeвestimator.py:973/1062/1093,cian_price_history.py:140на публичном loop'е через админ-ручку, бюджет на ветке acquire.