tradein: два места по-прежнему ходят мимо пула прокси (cian_price_history + /admin/scraper/health) #2830

Closed
opened 2026-08-11 06:09:08 +00:00 by lekss361 · 2 comments
Owner

Хвосты инцидента 2026-08-10, найдены при ревью fix/tradein-egress. Оба — в той же зоне поражения, но вне рамок того PR.

1. cian_price_history.py:112 — сбор мимо пула

fetch_detail(url, config=RealScraperConfig())

Без proxy_provider=scraper_kit'овский curl_proxy_url не активирует pool-режим и уходит на config.cian_proxy_url, то есть на статичный env-узел, не знающий про scrape_proxy_source_bans. Ровно тот механизм, из-за которого забаненный узел использовался месяц.

Путь: POST /admin/scrape/cian-price-history, запускается вручную, в планировщик намеренно не заведён — трафик низкий, поэтому и не блокер. Но баг того же класса и тихо воспроизведёт проблему, когда до него дойдут руки.

Что сделать: перевести на резолвер из app/services/proxy_egress.py (появится после мержа fix/tradein-egress) либо передавать proxy_provider.

2. admin.py:2301-2311, 2440 — health-страница показывает не тот прокси

_provider_proxy_url / GET /admin/scraper/health рапортуют settings.scraper_proxy_url / cian_proxy_url / yandex_proxy_url как «прокси источника» и пробуют его exit-IP через ipify.

После fix/tradein-egress реальный egress для переведённых вызывающих выбирается из пула по запросу и может отличаться от того, что показывает эта страница. То есть ops-панель будет показывать один узел, а трафик пойдёт через другой — ровно тот класс слепого пятна, который спрятал исходный инцидент, только теперь на диагностической странице.

Что сделать: показывать фактически выбираемый пулом узел для каждого источника (плюс, при желании, статичный fallback отдельной строкой с пометкой, что он используется только при пустом пуле).

Контекст

Корень инцидента: SCRAPER_PROXY_URL указывал на asocks-residential-1, забаненный в scrape_proxy_source_bans и для avito, и для cian до 13.08, при трёх здоровых мобильных узлах в пуле. Проявилось как «Cian: плохие куки» и «Avito: IP banned». Разбор — в vault.

Хвосты инцидента 2026-08-10, найдены при ревью `fix/tradein-egress`. Оба — в той же зоне поражения, но вне рамок того PR. ## 1. `cian_price_history.py:112` — сбор мимо пула ```python fetch_detail(url, config=RealScraperConfig()) ``` Без `proxy_provider=` — `scraper_kit`'овский `curl_proxy_url` не активирует pool-режим и уходит на `config.cian_proxy_url`, то есть на статичный env-узел, не знающий про `scrape_proxy_source_bans`. Ровно тот механизм, из-за которого забаненный узел использовался месяц. Путь: `POST /admin/scrape/cian-price-history`, запускается вручную, в планировщик намеренно не заведён — трафик низкий, поэтому и не блокер. Но баг того же класса и тихо воспроизведёт проблему, когда до него дойдут руки. **Что сделать:** перевести на резолвер из `app/services/proxy_egress.py` (появится после мержа `fix/tradein-egress`) либо передавать `proxy_provider`. ## 2. `admin.py:2301-2311, 2440` — health-страница показывает не тот прокси `_provider_proxy_url` / `GET /admin/scraper/health` рапортуют `settings.scraper_proxy_url` / `cian_proxy_url` / `yandex_proxy_url` как «прокси источника» и пробуют его exit-IP через ipify. После `fix/tradein-egress` реальный egress для переведённых вызывающих выбирается из пула **по запросу** и может отличаться от того, что показывает эта страница. То есть ops-панель будет показывать один узел, а трафик пойдёт через другой — ровно тот класс слепого пятна, который спрятал исходный инцидент, только теперь на диагностической странице. **Что сделать:** показывать фактически выбираемый пулом узел для каждого источника (плюс, при желании, статичный fallback отдельной строкой с пометкой, что он используется только при пустом пуле). ## Контекст Корень инцидента: `SCRAPER_PROXY_URL` указывал на `asocks-residential-1`, забаненный в `scrape_proxy_source_bans` и для avito, и для cian до 13.08, при трёх здоровых мобильных узлах в пуле. Проявилось как «Cian: плохие куки» и «Avito: IP banned». Разбор — в vault.
Collaborator

PR #2833 смержен и на проде (образ пересобран, tradein-backend перезапущен 11:00:33 UTC; проверено по коду в контейнере, не по SENTRY_RELEASE).

Поправка к моему же PR — цифра в описании неверна

В теле PR и в commit-message написано «resolved_zhk_url=0 во всех 59 прогонах» про третье место (resolve_cian_zhk_url_via_search). Это неправда, я проверил только 12 последних строк и обобщил. Полный подсчёт:

runs_total=62  with_resolved=9  with_failed_resolve=3

Резолв-нога срабатывала в 10 прогонах: 33 успешных резолва + 4 отказа, с 15.06 по 26.07 — то есть путь рабочий и просто простаивает 17 суток (окно ORDER BY h.id LIMIT 25 упирается в уже отрезолвленную голову; 93 дома с cian_zhk_url IS NULL ждут). Диагноз от этого не меняется — оборванная проводка, чинить, — но «спящая ветка» в описании занижает важность: 33 ЖК-url были собраны через статичный узел, мимо scrape_proxy_source_bans.

Про 4 отказа (failed_resolve): были ли среди них 403 от отбитого узла — не установлено, логи прода за те даты не сохранились. До правки 403 там возвращался как None, поэтому в счётчиках он неотличим от «пустой SERP».

Прод-верификация, числа

1. cian_price_history — запущен точечно (listing_id=10376603, ровно ОДИН запрос к Циану):

2026-08-12 11:05:38 proxy_pool: leased proxy id=9 provider=cian by=-1
2026-08-12 11:05:40 proxy_pool: mark_health id=9 ok=True
2026-08-12 11:05:40 proxy_pool: released proxy id=9
cian_price_history done: checked=1 saved=0 skipped=1 errors=0 2.6s

Строк proxy_pool: leased в tradein-backend было 0 (за всё время жизни контейнера), стало 1 — путь впервые взял узел из пула и вернул вердикт. Заодно снята предпосылка «куки Циана мертвы, значит и с пулом не поедет»: detail-страница отдалась за 2.6 с без единой куки (errors=0; skipped = у этого лота просто нет priceChanges).

2. /admin/scraper/health — до правки все три источника показывали 190.2.145.131:10313 (= SCRAPER_PROXY_URL), после:

avito/cian/yandex → 109.236.82.42:11048  (asocks-mobile-3, id 11), exit_ip 92.100.81.232
+ 3 строки на опрос: proxy_egress: source=… -> pool proxy id=11 (asocks-mobile-3)

Узел 1 (asocks-residential-1) забанен для cian до 15.08 и для avito до 13.08 — панель его больше и не покажет.

Остаточное расхождение, честно: proxy_egress (панель) сортирует last_ok_at DESC, а proxy_pool.acquire (боевые lease-пути) — last_ok_at NULLS LAST по возрастанию. Поэтому панель показала id=11, а реальный lease price-history взял id=9. Оба — из пула и с учётом банов, но панель не гарантирует ИМЕННО тот узел, который возьмёт lease-путь. Если нужна точность — отдельная задача: показывать весь допустимый набор по источнику либо явно называть, чей это выбор.

Критерий приёмки для третьего места — записан ДО факта

Ближайший newbuilding_enrich: 2026-08-13 00:44:16 UTC, limit=25, force=false.

  • На этом прогоне resolved_zhk_url=0 / failed_resolve=0 ОЖИДАЕМЫ (окно упирается в уже отрезолвленные дома) — читать это как «фикс работает» нельзя, ветка просто не входится.
  • Правка становится наблюдаемой только на прогоне, куда попал дом без cian_zhk_url: нужен limit>=40 (39 домов с url, 93 без) либо точечный запуск.
  • Приёмка тогда: resolved_zhk_url + failed_resolve > 0 И в логах tradein-scraper строка proxy_pool: leased proxy id=N provider=cian вплотную перед resolve_cian_zhk_url_via_search nb_id=…. До правки резолв-нога lease не брала никогда.

Про «ровно два места»

Мест оказалось четыре: два ваших + resolve_cian_zhk_url_via_search (жив, см. выше) + мёртвый resolve_cian_zhk_url (0 вызывающих, /zhk/<id>/ 404-ит с #972, egress тоже был мимо пула) — последний удалён, а не починен.

Отдельно: одного proxy_provider= для ручки price-history было бы мало. USE_PROXY_POOL_CURL: "true" задан только контейнеру scraper, а ручка живёт в backend — без флага curl_proxy_url игнорирует провайдер и уходит на env. Правка выглядела бы зелёной и не делала бы ничего.

PR #2833 смержен и на проде (образ пересобран, `tradein-backend` перезапущен 11:00:33 UTC; проверено по коду в контейнере, не по SENTRY_RELEASE). ## Поправка к моему же PR — цифра в описании неверна В теле PR и в commit-message написано «`resolved_zhk_url=0` во всех 59 прогонах» про третье место (`resolve_cian_zhk_url_via_search`). **Это неправда, я проверил только 12 последних строк и обобщил.** Полный подсчёт: ``` runs_total=62 with_resolved=9 with_failed_resolve=3 ``` Резолв-нога срабатывала в **10 прогонах**: 33 успешных резолва + 4 отказа, с 15.06 по **26.07** — то есть путь рабочий и просто простаивает 17 суток (окно `ORDER BY h.id LIMIT 25` упирается в уже отрезолвленную голову; 93 дома с `cian_zhk_url IS NULL` ждут). Диагноз от этого не меняется — оборванная проводка, чинить, — но «спящая ветка» в описании занижает важность: 33 ЖК-url были собраны через статичный узел, мимо `scrape_proxy_source_bans`. Про 4 отказа (`failed_resolve`): были ли среди них 403 от отбитого узла — **не установлено**, логи прода за те даты не сохранились. До правки 403 там возвращался как `None`, поэтому в счётчиках он неотличим от «пустой SERP». ## Прод-верификация, числа **1. `cian_price_history`** — запущен точечно (`listing_id=10376603`, ровно ОДИН запрос к Циану): ``` 2026-08-12 11:05:38 proxy_pool: leased proxy id=9 provider=cian by=-1 2026-08-12 11:05:40 proxy_pool: mark_health id=9 ok=True 2026-08-12 11:05:40 proxy_pool: released proxy id=9 cian_price_history done: checked=1 saved=0 skipped=1 errors=0 2.6s ``` Строк `proxy_pool: leased` в `tradein-backend` было **0** (за всё время жизни контейнера), стало **1** — путь впервые взял узел из пула и вернул вердикт. Заодно снята предпосылка «куки Циана мертвы, значит и с пулом не поедет»: detail-страница отдалась за 2.6 с **без единой куки** (`errors=0`; `skipped` = у этого лота просто нет `priceChanges`). **2. `/admin/scraper/health`** — до правки все три источника показывали `190.2.145.131:10313` (= `SCRAPER_PROXY_URL`), после: ``` avito/cian/yandex → 109.236.82.42:11048 (asocks-mobile-3, id 11), exit_ip 92.100.81.232 + 3 строки на опрос: proxy_egress: source=… -> pool proxy id=11 (asocks-mobile-3) ``` Узел 1 (`asocks-residential-1`) забанен для cian до 15.08 и для avito до 13.08 — панель его больше и не покажет. **Остаточное расхождение, честно:** `proxy_egress` (панель) сортирует `last_ok_at DESC`, а `proxy_pool.acquire` (боевые lease-пути) — `last_ok_at NULLS LAST` по возрастанию. Поэтому панель показала id=11, а реальный lease price-history взял id=9. Оба — из пула и с учётом банов, но панель не гарантирует ИМЕННО тот узел, который возьмёт lease-путь. Если нужна точность — отдельная задача: показывать весь допустимый набор по источнику либо явно называть, чей это выбор. ## Критерий приёмки для третьего места — записан ДО факта Ближайший `newbuilding_enrich`: **2026-08-13 00:44:16 UTC**, `limit=25, force=false`. - На этом прогоне `resolved_zhk_url=0` / `failed_resolve=0` ОЖИДАЕМЫ (окно упирается в уже отрезолвленные дома) — **читать это как «фикс работает» нельзя**, ветка просто не входится. - Правка становится наблюдаемой только на прогоне, куда попал дом без `cian_zhk_url`: нужен `limit>=40` (39 домов с url, 93 без) либо точечный запуск. - Приёмка тогда: `resolved_zhk_url + failed_resolve > 0` И в логах `tradein-scraper` строка `proxy_pool: leased proxy id=N provider=cian` вплотную перед `resolve_cian_zhk_url_via_search nb_id=…`. До правки резолв-нога lease не брала никогда. ## Про «ровно два места» Мест оказалось **четыре**: два ваших + `resolve_cian_zhk_url_via_search` (жив, см. выше) + мёртвый `resolve_cian_zhk_url` (0 вызывающих, `/zhk/<id>/` 404-ит с #972, egress тоже был мимо пула) — последний удалён, а не починен. Отдельно: одного `proxy_provider=` для ручки price-history было бы мало. `USE_PROXY_POOL_CURL: "true"` задан только контейнеру `scraper`, а ручка живёт в `backend` — без флага `curl_proxy_url` игнорирует провайдер и уходит на env. Правка выглядела бы зелёной и не делала бы ничего.
lekss361 added the
bug
scope/backend
scrapers
tradein
labels 2026-08-16 10:25:22 +00:00
Author
Owner

Закрываю по итогам разбора трекера 16.08.2026

Вердикт: сделано кодом.

Оба хвоста инцидента 10.08 закрыты смерженным PR #2833.

Доказательство: e17687ae fix(tradein/scrapers): убрать оставшиеся обходы пула прокси (#2830) (#2833)

Независимая проверка. Вердикт отдельно проверялся вторым проходом, задачей которого было именно опровергнуть закрытие, а не подтвердить его:

Оба названных в задаче места закрыты в e17687ae (в main), причём с обходом ловушки, которая сделала бы правку пустой: cian_price_history.py объявляет _PoolCurlConfig с принудительным use_proxy_pool_curl=True — иначе proxy_provider игнорировался бы, потому что USE_PROXY_POOL_CURL задан только контейнеру scraper (проверил printenv на проде: у tradein-backend его нет, у tradein-scraper есть); admin.py::_provider_proxy_url теперь резолвит узел через app/services/proxy_egress.resolve_proxy_url с учётом scrape_proxy_source_bans и fail-closed вместо статичного settings.scraper_proxy_url. Есть регрессионный тест test_2830_pool_bypass_tails.py. Мест по факту оказалось четыре (комментарий автора): resolve_cian_zhk_url_via_search тоже переведён (newbuilding_enrich_backfill.py:497 передаёт proxy_provider), мёртвый resolve_cian_zhk_url удалён. Критерий приёмки третьего места де-факто выполнен: прогон newbuilding_enrich 14.08 00:17 дал resolved_zhk_url=1 (резолв-нога отработала уже после правки, failed_resolve=0). Остаточное расхождение, честно названное автором и НЕ отменяющее предмет задачи: панель сортирует кандидатов резолвером proxy_egress (ban_count/last_ok_at DESC), а lease-пути — proxy_pool.acquire (last_ok_at ASC), поэтому панель гарантирует 'узел из пула с учётом банов', но не обязательно тот самый, что возьмёт lease; автор предлагал это как отдельную задачу.

Если что-то из перечисленного всё же живо — переоткройте задачу, разбор мог упустить частный случай.

## Закрываю по итогам разбора трекера 16.08.2026 **Вердикт:** сделано кодом. Оба хвоста инцидента 10.08 закрыты смерженным PR #2833. **Доказательство:** e17687ae fix(tradein/scrapers): убрать оставшиеся обходы пула прокси (#2830) (#2833) **Независимая проверка.** Вердикт отдельно проверялся вторым проходом, задачей которого было именно опровергнуть закрытие, а не подтвердить его: > Оба названных в задаче места закрыты в e17687ae (в main), причём с обходом ловушки, которая сделала бы правку пустой: cian_price_history.py объявляет _PoolCurlConfig с принудительным use_proxy_pool_curl=True — иначе proxy_provider игнорировался бы, потому что USE_PROXY_POOL_CURL задан только контейнеру scraper (проверил printenv на проде: у tradein-backend его нет, у tradein-scraper есть); admin.py::_provider_proxy_url теперь резолвит узел через app/services/proxy_egress.resolve_proxy_url с учётом scrape_proxy_source_bans и fail-closed вместо статичного settings.scraper_proxy_url. Есть регрессионный тест test_2830_pool_bypass_tails.py. Мест по факту оказалось четыре (комментарий автора): resolve_cian_zhk_url_via_search тоже переведён (newbuilding_enrich_backfill.py:497 передаёт proxy_provider), мёртвый resolve_cian_zhk_url удалён. Критерий приёмки третьего места де-факто выполнен: прогон newbuilding_enrich 14.08 00:17 дал resolved_zhk_url=1 (резолв-нога отработала уже после правки, failed_resolve=0). Остаточное расхождение, честно названное автором и НЕ отменяющее предмет задачи: панель сортирует кандидатов резолвером proxy_egress (ban_count/last_ok_at DESC), а lease-пути — proxy_pool.acquire (last_ok_at ASC), поэтому панель гарантирует 'узел из пула с учётом банов', но не обязательно тот самый, что возьмёт lease; автор предлагал это как отдельную задачу. Если что-то из перечисленного всё же живо — переоткройте задачу, разбор мог упустить частный случай.
Sign in to join this conversation.
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#2830
No description provided.