fix(tradein/estimator): IMV-путь /estimate через пул прокси; пустой пул — честная деградация без IMV (#3386 п.в) #3397

Merged
bot-backend merged 2 commits from fix/3386-estimator-imv-proxy-pool into main 2026-09-06 06:04:08 +00:00
Collaborator

Часть #3386 (п. в). Не закрывает issue.

Поправка к постановке. IMV из эстиматора — curl_cffi, не сайдкар: _get_or_fetch_imv_cached → evaluate_via_imv без browser_fetcher (флаг avito_imv_use_browser_fetcher читает только house_imv_backfill). 4 запроса одной AsyncSession (warm-up → coords → geo/position → realty-imv/get-data). Без proxy_provider curl_proxy_url: use_pool = флаг AND provider is not None → False → proxies = SCRAPER_PROXY_URL (выключенный узел 9, 407) → Авито отвечает 403/ошибка → /estimate без IMV-якоря с момента смерти узла (02.09). История «сайдкар держит чужую аренду» относится к бэкфиллу, не к /estimate.

Фикс (app/services/estimator.py:786, :819): proxy_provider=RealProxyProvider() (lazy import как в house_imv_backfill.py:744-755). Lease — один на вызов evaluate_via_imv (providers/_proxy.py:57-135 через ExitStack в imv.py:497-534), release гарантирован finally (mark_banned → mark_health → release, каждый в своём try) на штатном выходе, исключении и таймауте _with_budget.

Деградация: NoProxyAvailableError поднимается до HTTP из __enter__, доходит до except Exception в _get_or_fetch_imv_cachedNone/estimate 200 без блока avito_imv; добавлена развилка caused_by_no_proxy(e) → лог imv: пул прокси пуст — estimator продолжает без IMV вместо fetch failed (читалось как «Авито отвалился»). Поля imv_error_reason в ответе /estimate нет и не было (колонка houses) — не добавлял. acquire синхронный (SELECT FOR UPDATE SKIP LOCKED + UPDATE, мс) внутри kit — как у соседей; та же корутина уже делает синхронный кэш-лукап (#3083 — потолок назван).

Тесты tests/test_3386_estimator_imv_proxy_pool.py (5): оба вызова получают proxy_provider is not None; пустой пул в production → результат без IMV, без исключения, честный лог; lease освобождён ровно один раз на успехе и на ошибке IMV.

Фальсификация (git apply -R estimator.py): 5 failed по значению — assert None is not None (kwargs без proxy_provider) ×2, лог fetch failed вместо «пул прокси пуст», acquired == 0 ×2. Восстановлено.

Прогоны: полный backend 5535 passed, 35 skipped (rc=0); ruff OK.

Приёмка на проде: (1) printenv USE_PROXY_POOL_CURL = true в backend (для эстиматора важен именно curl-флаг); (2) POST /estimate изнутри контейнера на адресе без IMV-кэша за 24 ч → блок avito_imv в ответе, в логах imv: cache MISS … → imv: fresh recommended=…; (3) во время вызова у узла affinity IN ('avito','any') leased_by занят, после — свободен; (4) отрицательный контроль — сравнить долю оценок с IMV-якорем до/после по логам (fresh recommended vs fetch failed).

Часть #3386 (п. в). Не закрывает issue. **Поправка к постановке.** IMV из эстиматора — **curl_cffi, не сайдкар**: `_get_or_fetch_imv_cached → evaluate_via_imv` без `browser_fetcher` (флаг `avito_imv_use_browser_fetcher` читает только `house_imv_backfill`). 4 запроса одной `AsyncSession` (warm-up → coords → geo/position → realty-imv/get-data). Без `proxy_provider` `curl_proxy_url`: `use_pool = флаг AND provider is not None` → False → `proxies = SCRAPER_PROXY_URL` (выключенный узел 9, 407) → Авито отвечает 403/ошибка → **`/estimate` без IMV-якоря** с момента смерти узла (02.09). История «сайдкар держит чужую аренду» относится к бэкфиллу, не к `/estimate`. **Фикс** (`app/services/estimator.py:786, :819`): `proxy_provider=RealProxyProvider()` (lazy import как в `house_imv_backfill.py:744-755`). Lease — один на вызов `evaluate_via_imv` (`providers/_proxy.py:57-135` через `ExitStack` в `imv.py:497-534`), release гарантирован `finally` (mark_banned → mark_health → release, каждый в своём try) на штатном выходе, исключении и таймауте `_with_budget`. **Деградация:** `NoProxyAvailableError` поднимается до HTTP из `__enter__`, доходит до `except Exception` в `_get_or_fetch_imv_cached` → `None` → `/estimate` 200 без блока `avito_imv`; добавлена развилка `caused_by_no_proxy(e)` → лог `imv: пул прокси пуст — estimator продолжает без IMV` вместо `fetch failed` (читалось как «Авито отвалился»). Поля `imv_error_reason` в ответе `/estimate` нет и не было (колонка `houses`) — не добавлял. `acquire` синхронный (SELECT FOR UPDATE SKIP LOCKED + UPDATE, мс) внутри kit — как у соседей; та же корутина уже делает синхронный кэш-лукап (#3083 — потолок назван). **Тесты** `tests/test_3386_estimator_imv_proxy_pool.py` (5): оба вызова получают `proxy_provider is not None`; пустой пул в production → результат без IMV, без исключения, честный лог; lease освобождён ровно один раз на успехе и на ошибке IMV. **Фальсификация** (`git apply -R` estimator.py): 5 failed по значению — `assert None is not None` (kwargs без `proxy_provider`) ×2, лог `fetch failed` вместо «пул прокси пуст», `acquired == 0` ×2. Восстановлено. **Прогоны:** полный backend `5535 passed, 35 skipped` (rc=0); ruff OK. **Приёмка на проде:** (1) `printenv USE_PROXY_POOL_CURL` = true в backend (для эстиматора важен именно curl-флаг); (2) `POST /estimate` изнутри контейнера на адресе без IMV-кэша за 24 ч → блок `avito_imv` в ответе, в логах `imv: cache MISS … → imv: fresh recommended=…`; (3) во время вызова у узла `affinity IN ('avito','any')` `leased_by` занят, после — свободен; (4) отрицательный контроль — сравнить долю оценок с IMV-якорем до/после по логам (`fresh recommended` vs `fetch failed`).
bot-backend added 1 commit 2026-09-06 05:34:55 +00:00
fix(tradein/estimator): IMV-путь берёт прокси из пула, пустой пул не ломает /estimate (#3386)
All checks were successful
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Successful in 4m55s
6f97995140
Оба вызова `evaluate_via_imv` в `_get_or_fetch_imv_cached` шли без
`proxy_provider`, а `providers/_proxy.py::curl_proxy_url` считает
`use_pool = флаг AND provider is not None` — пул был выключен по построению,
curl-сессия уходила на env-прокси SCRAPER_PROXY_URL (мёртвый узел, #2613).

Провайдер берётся из уже существующего module-level импорта
`app.services.scraper_adapters` (в estimator цикла нет, в отличие от
house_imv_backfill — там lazy import вынужденный). Lease — один на вызов IMV,
acquire/release внутри `curl_proxy_url`, release в finally на всех выходах.

Пустой пул в проде (`NoProxyAvailableError`, в т.ч. завёрнутый — проверка по
цепочке причин `caused_by_no_proxy`) остаётся graceful: `_get_or_fetch_imv_cached`
возвращает None, ответ отдаётся без IMV-якоря. Причина в логе теперь честная —
«пул прокси пуст», а не «fetch failed» (запрос не уходил вовсе).
Author
Collaborator

Code-review (06.09): ⚠️ minor. Проверено: проводка настоящая (use_proxy_pool_curl/environment у RealScraperConfig есть — scraper_adapters.py:194/204), NoProxyAvailableError → 200 без IMV (тест по значению), SKIP LOCKED в acquire — на блокировке строки не ждёт, lease не течёт (release в finally на всех выходах, тест через настоящий curl_proxy_url), сессия роута не отравляется (RealProxyProvider — своя SessionLocal), lazy-импорта не нужно (estimator.py:83), контроль на main красный по значению.

Докатываю до мержа: except Exceptionexcept BaseException в _proxy.py:127 (на CancelledError от _with_budget узел записывался ok=True — дефект старый, досягаемым делает этот PR) + тест на отмену; create=True из патчей теста; admin.py:1098 IMV через _kit_proxy_provider(). Хвосты (statement_timeout/зависание вне бюджета, атрибуция IMV-банов источнику avito — решение владельца, house_imv_backfill.py:929) — #3398.

Code-review (06.09): ⚠️ minor. Проверено: проводка настоящая (`use_proxy_pool_curl`/`environment` у `RealScraperConfig` есть — `scraper_adapters.py:194/204`), `NoProxyAvailableError` → 200 без IMV (тест по значению), `SKIP LOCKED` в acquire — на блокировке строки не ждёт, lease не течёт (release в `finally` на всех выходах, тест через настоящий `curl_proxy_url`), сессия роута не отравляется (`RealProxyProvider` — своя `SessionLocal`), lazy-импорта не нужно (`estimator.py:83`), контроль на main красный по значению. Докатываю до мержа: `except Exception` → `except BaseException` в `_proxy.py:127` (на `CancelledError` от `_with_budget` узел записывался `ok=True` — дефект старый, досягаемым делает этот PR) + тест на отмену; `create=True` из патчей теста; `admin.py:1098` IMV через `_kit_proxy_provider()`. Хвосты (statement_timeout/зависание вне бюджета, атрибуция IMV-банов источнику avito — решение владельца, `house_imv_backfill.py:929`) — #3398.
Light1YT added 1 commit 2026-09-06 05:57:28 +00:00
fix(#3386): curl_proxy_url — health=False и на отмене (BaseException); admin IMV через пул; тест без create=True
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m57s
33cad4f82e
bot-backend merged commit 075cec4b57 into main 2026-09-06 06:04:08 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3397
No description provided.