fix(tradein/proxy): проба спрашивает каждую площадку, вердикт пишется на пару «узел × источник» (#2800 B) #2803

Merged
bot-backend merged 1 commit from fix/2800b-per-source-probe into main 2026-08-09 18:28:38 +00:00
Collaborator

Что не так

#2723 починил транспорт пробы — ходить браузером, как ходит работа. Ходила она при этом для всех узлов на один зашитый адрес, robots.txt Авито. Прокси-узел не «жив/мёртв» вообще: он годен или не годен конкретной площадке.

Живой замер 09.08.2026 тем же трактом (сайдкар tradein-browser → camoufox с этим прокси → навигация):

узел affinity avito ekb.cian.ru realty.yandex.ru bff-search-web.domclick.ru
1 domclick 200 200 «Ошибка — Циан» (374 КБ HTML) 200 500 NS_ERROR_PROXY_BAD_GATEWAY
9 any 200 200 200 200
10 any 200 200 200 200
11 any 200 200 200 200

У узла 1 при этом browser_fail_streak=0 и свежая проба. domclick_city_sweep — ноль лотов четвёртые сутки.

Два уточнения к исходной гипотезе, найденные замером:

  • узел мёртв не «для Домклика», а для его рабочего хоста: domclick.ru/robots.txt через него отдаёт 200, bff-search-web.domclick.ru/robots.txt — 500. Проба по apex-домену была бы зелёной и бесполезной — та же ошибка «проба идёт не рабочим путём», на третьей оси;
  • зелёная проба Циана через узел 1 — тоже ложная: HTTP 200, непустой HTML, но это страница «Ошибка — Циан», а не robots.txt. Старая логика (ok = html непустой) засчитывала её как успех.

Что сделано

  1. Адрес на каждый источник, по рабочему хосту (_PROBE_URLS в browser_fetcher.py): www.avito.ru, ekb.cian.ru, realty.yandex.ru, bff-search-web.domclick.ru. probe_proxy_via_browser(..., source=...) выбирает адрес; source в теле /fetch остаётся 'generic' — проба по-прежнему не отбирает лок у боевого инстанса camoufox.
  2. 200 ≠ «площадка ответила»: маркер User-agent есть в robots.txt всех четырёх рабочих хостов (проверено вживую) и отсутствует в заглушке. Такой ответ теперь fail_kind="page".
  3. Вердикт пары — в существующую scrape_proxy_source_bans (её уже читает acquire(source)). Новой сущности и миграции нет. Свои строки помечены reason='probe:browser'; clear_source_bans(..., only_reason=...) даёт пробе снимать только их. Бан, распознанный боевым сбором, проба не гасит: robots.txt площадка отдаёт и забаненному IP, и позволить дешёвому успеху стирать дорогой вердикт значило бы повторить #2723 на паре.
  4. Узловой вердикт сохранил смысл «браузерный тракт через узел не работает вообще»: считается по итогу всего креста — хоть одна зелёная площадка → ok, все красные транспортом → провал узла. Отказ одной площадки узел глобально не пятнает.
  5. Подтверждение отказа пары немедленным повтором (браузер уже поднят, секунды, только на отказах): один флаки-запуск camoufox не должен стоить паре 6 часов.

Порог отключения не поднимался, affinity ни у кого не снималась — симптом не заметается.

Цена (посчитана до правки, замером)

было стало
навигаций за такт 4 узла × 1 адрес 4 узла × 4 источника = 16
в сутки 16 64, т.е. 16 robots.txt на площадку против ~1000 боевых /fetch
прогонов healthcheck с браузерной пробой 4/сут 4/сут (такт BROWSER_PROBE_MINUTES=360 не тронут)
длительность такта с пробой ~50 с ~3 мин (замер: 9–18 с на навигацию)

Крест ограничен источниками, которым узел вообще может достаться (provider_affinity IN (source,'any') — тот же предикат, что у acquire). Разрежённая схема (по одному источнику за такт, round-robin) рассматривалась и отвергнута числом: вердикт пары протухал бы до 24 ч при бане в 6 ч — окно, в котором acquire снова выдаёт узел, не спросив.

Тесты

Новый tests/test_2800_per_source_probe.py12 из 14 красные на origin/main, прогон приложен ниже. Главный краснеет на настоящей поломке, а не на отсутствующем ключе счётчика:

>       assert first is not None and first.id == 2
E       AssertionError: assert (ProxyLease(id=1, url='http://u:p@h1:8080', ...) is not None and 1 == 2)

то есть на старом коде acquire('domclick') выдаёт ровно тот узел, у которого рабочий хост Домклика отвечает NS_ERROR_PROXY_BAD_GATEWAY.

Два теста проходят на main и оставлены как сторожа несломанного: «одна мёртвая площадка не пятнает узел» и «все мёртвые — пятнают» (#2723 цел).

Два теста #2723 обновлены осознанно: они считали вызовы пробы за прогон (1 → 4). Семантика такта не менялась — изменилась цена одного прогона, и она теперь в них записана числом.

Локально: pytest tests/test_2800_per_source_probe.py tests/test_2723_browser_probe.py tests/services/test_proxy_pool.py tests/services/test_proxy_rotation.py tests/test_2657_domclick_proxy_pool.py tests/test_admin_proxies.py → 196 passed. Полный прогон совпадает с базой origin/main (11 падений локальной среды, одинаковые до и после).

Прод-верификация (критерий записан ДО факта)

proxy_healthcheck идёт каждые 30 минут, браузерный крест — по гейту browser_check_at (≤6 ч после деплоя). Критерии:

  1. ≤6 ч после деплоя в логах tradein-scraper появляется строка healthcheck done — … pair_checked=16 … (было: browser_checked=4 без пары).
  2. ≤6 ч в scrape_proxy_source_bans появляется строка (proxy_id=1, source='domclick', reason='probe:browser') — раздельный вердикт по источникам у узла, у которого Авито зелёный.
  3. 10.08 04:43:59Z — ближайший domclick_city_sweep (scrape_schedules.next_run_at): узел 1 в прогон не попадает, acquire('domclick') его не выдаёт. Признак в логах — отсутствие leased proxy id=1 provider=domclick.

Пункт 3 — не обещание, что Домклик соберётся: у него могут быть свои беды помимо узла. Проверяемое утверждение ровно одно: первый бакет не тратится на узел, который до площадки не доходит.

Refs #2800

## Что не так #2723 починил **транспорт** пробы — ходить браузером, как ходит работа. Ходила она при этом для всех узлов на **один зашитый адрес**, `robots.txt` Авито. Прокси-узел не «жив/мёртв» вообще: он годен или не годен **конкретной площадке**. Живой замер 09.08.2026 тем же трактом (сайдкар `tradein-browser` → camoufox с этим прокси → навигация): | узел | affinity | avito | ekb.cian.ru | realty.yandex.ru | bff-search-web.domclick.ru | |---|---|---|---|---|---| | **1** | domclick | 200 | **200 «Ошибка — Циан»** (374 КБ HTML) | 200 | **500 `NS_ERROR_PROXY_BAD_GATEWAY`** | | 9 | any | 200 | 200 | 200 | 200 | | 10 | any | 200 | 200 | 200 | 200 | | 11 | any | 200 | 200 | 200 | 200 | У узла 1 при этом `browser_fail_streak=0` и свежая проба. `domclick_city_sweep` — ноль лотов четвёртые сутки. Два уточнения к исходной гипотезе, найденные замером: - узел мёртв **не «для Домклика»**, а для его рабочего хоста: `domclick.ru/robots.txt` через него отдаёт **200**, `bff-search-web.domclick.ru/robots.txt` — 500. Проба по apex-домену была бы зелёной и бесполезной — та же ошибка «проба идёт не рабочим путём», на третьей оси; - зелёная проба Циана через узел 1 — тоже ложная: HTTP 200, непустой HTML, но это страница «Ошибка — Циан», а не robots.txt. Старая логика (`ok = html непустой`) засчитывала её как успех. ## Что сделано 1. **Адрес на каждый источник, по рабочему хосту** (`_PROBE_URLS` в `browser_fetcher.py`): `www.avito.ru`, `ekb.cian.ru`, `realty.yandex.ru`, `bff-search-web.domclick.ru`. `probe_proxy_via_browser(..., source=...)` выбирает адрес; `source` в теле `/fetch` остаётся `'generic'` — проба по-прежнему не отбирает лок у боевого инстанса camoufox. 2. **200 ≠ «площадка ответила»**: маркер `User-agent` есть в robots.txt всех четырёх рабочих хостов (проверено вживую) и отсутствует в заглушке. Такой ответ теперь `fail_kind="page"`. 3. **Вердикт пары — в существующую `scrape_proxy_source_bans`** (её уже читает `acquire(source)`). Новой сущности и **миграции нет**. Свои строки помечены `reason='probe:browser'`; `clear_source_bans(..., only_reason=...)` даёт пробе снимать **только их**. Бан, распознанный боевым сбором, проба не гасит: robots.txt площадка отдаёт и забаненному IP, и позволить дешёвому успеху стирать дорогой вердикт значило бы повторить #2723 на паре. 4. **Узловой вердикт сохранил смысл** «браузерный тракт через узел не работает вообще»: считается по итогу **всего креста** — хоть одна зелёная площадка → ok, все красные транспортом → провал узла. Отказ одной площадки узел глобально не пятнает. 5. **Подтверждение отказа пары немедленным повтором** (браузер уже поднят, секунды, только на отказах): один флаки-запуск camoufox не должен стоить паре 6 часов. Порог отключения не поднимался, affinity ни у кого не снималась — симптом не заметается. ## Цена (посчитана до правки, замером) | | было | стало | |---|---|---| | навигаций за такт | 4 узла × 1 адрес | 4 узла × 4 источника = 16 | | в сутки | 16 | 64, т.е. **16 robots.txt на площадку** против ~1000 боевых `/fetch` | | прогонов healthcheck с браузерной пробой | 4/сут | 4/сут (такт `BROWSER_PROBE_MINUTES=360` не тронут) | | длительность такта с пробой | ~50 с | ~3 мин (замер: 9–18 с на навигацию) | Крест ограничен источниками, которым узел вообще может достаться (`provider_affinity IN (source,'any')` — тот же предикат, что у `acquire`). Разрежённая схема (по одному источнику за такт, round-robin) рассматривалась и отвергнута числом: вердикт пары протухал бы до 24 ч при бане в 6 ч — окно, в котором `acquire` снова выдаёт узел, не спросив. ## Тесты Новый `tests/test_2800_per_source_probe.py` — **12 из 14 красные на `origin/main`**, прогон приложен ниже. Главный краснеет на настоящей поломке, а не на отсутствующем ключе счётчика: ``` > assert first is not None and first.id == 2 E AssertionError: assert (ProxyLease(id=1, url='http://u:p@h1:8080', ...) is not None and 1 == 2) ``` то есть на старом коде `acquire('domclick')` выдаёт ровно тот узел, у которого рабочий хост Домклика отвечает `NS_ERROR_PROXY_BAD_GATEWAY`. Два теста проходят на main и оставлены как сторожа несломанного: «одна мёртвая площадка не пятнает узел» и «все мёртвые — пятнают» (#2723 цел). Два теста #2723 обновлены **осознанно**: они считали вызовы пробы за прогон (1 → 4). Семантика такта не менялась — изменилась цена одного прогона, и она теперь в них записана числом. Локально: `pytest tests/test_2800_per_source_probe.py tests/test_2723_browser_probe.py tests/services/test_proxy_pool.py tests/services/test_proxy_rotation.py tests/test_2657_domclick_proxy_pool.py tests/test_admin_proxies.py` → 196 passed. Полный прогон совпадает с базой origin/main (11 падений локальной среды, одинаковые до и после). ## Прод-верификация (критерий записан ДО факта) `proxy_healthcheck` идёт каждые 30 минут, браузерный крест — по гейту `browser_check_at` (≤6 ч после деплоя). Критерии: 1. **≤6 ч после деплоя** в логах `tradein-scraper` появляется строка `healthcheck done — … pair_checked=16 …` (было: `browser_checked=4` без пары). 2. **≤6 ч** в `scrape_proxy_source_bans` появляется строка `(proxy_id=1, source='domclick', reason='probe:browser')` — раздельный вердикт по источникам у узла, у которого Авито зелёный. 3. **10.08 04:43:59Z** — ближайший `domclick_city_sweep` (`scrape_schedules.next_run_at`): узел 1 в прогон не попадает, `acquire('domclick')` его не выдаёт. Признак в логах — отсутствие `leased proxy id=1 provider=domclick`. Пункт 3 — не обещание, что Домклик соберётся: у него могут быть свои беды помимо узла. Проверяемое утверждение ровно одно: **первый бакет не тратится на узел, который до площадки не доходит.** Refs #2800
bot-backend added 1 commit 2026-08-09 18:13:30 +00:00
fix(tradein/proxy): проба спрашивает каждую площадку, вердикт пишется на пару «узел × источник» (#2800 B)
All checks were successful
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
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 Trade-In / backend-tests (pull_request) Successful in 3m53s
2e30fbe8e3
#2723 починил ТРАНСПОРТ пробы — ходить браузером, как ходит работа. Ходила она при
этом для всех узлов на один зашитый адрес, robots.txt Авито. Прокси-узел не
«жив/мёртв» вообще: замер на проде 09.08.2026 тем же трактом —

  узел | affinity | avito | ekb.cian.ru         | realty.ya.ru | bff-search-web.domclick.ru
  -----+----------+-------+---------------------+--------------+---------------------------
    1  | domclick | 200   | 200 «Ошибка — Циан» | 200          | 500 NS_ERROR_PROXY_BAD_GATEWAY
    9  | any      | 200   | 200                 | 200          | 200
   10  | any      | 200   | 200                 | 200          | 200
   11  | any      | 200   | 200                 | 200          | 200

— у узла 1 при этом browser_fail_streak=0 и свежая проба. Зелёный ответ означал
«годен для Авито», а читался как «годен».

Что сделано:

1. Адрес пробы на КАЖДЫЙ источник и по РАБОЧЕМУ хосту площадки. Apex не годится:
   через тот же узел domclick.ru отвечает 200, а bff-search-web.domclick.ru, куда
   ходит сбор, — 500. Проба по apex была бы зелёной и бесполезной.
2. HTTP 200 с непустым телом больше не значит «площадка ответила»: robots.txt всех
   четырёх рабочих хостов содержит «User-agent», страница-заглушка Циана (374 КБ,
   «Ошибка — Циан») — нет. Раньше такой ответ засчитывался как успех.
3. Вердикт пары пишется в СУЩЕСТВУЮЩУЮ scrape_proxy_source_bans (её уже читает
   acquire(source)) — новой сущности и миграции нет. Свои строки помечены
   reason='probe:browser'; успешная проба снимает ТОЛЬКО их. Бан, распознанный
   боевым сбором, проба не гасит: robots.txt площадка отдаёт и забаненному IP, и
   позволить дешёвому успеху стирать дорогой вердикт значило бы повторить #2723.
4. Узловой вердикт (browser_fail_streak/browser_unfit_since) сохранил смысл
   «браузерный тракт через узел не работает вообще»: считается по итогу ВСЕГО
   креста — хоть одна зелёная площадка → ok, все красные транспортом → провал узла.
   Отказ одной площадки узел глобально не пятнает.
5. Транспортный отказ пары подтверждается немедленным повтором (браузер уже поднят,
   секунды, только на отказах) — один флаки-запуск camoufox не стоит паре 6 часов.

Цена, посчитана до правки: было 4 узла × 1 адрес / 360 мин = 16 навигаций в сутки,
стало 4 × 4 = 64, то есть 16 robots.txt НА ПЛОЩАДКУ в сутки против ~1000 боевых
/fetch. Такт (BROWSER_PROBE_MINUTES=360) не изменён, прогонов с браузерной пробой
по-прежнему 4 в сутки; крест ограничен источниками, которым узел может достаться.
Разрежённая схема (по источнику за такт, round-robin) отвергнута: вердикт пары
протухал бы до 24 ч при бане в 6 ч — окно, в котором acquire снова выдаёт узел.

Два теста #2723 обновлены осознанно: они считали вызовы пробы за прогон (1 → 4),
семантика такта не менялась.

Refs #2800
bot-backend merged commit 08bb9d6549 into main 2026-08-09 18:28:38 +00:00
bot-backend deleted branch fix/2800b-per-source-probe 2026-08-09 18:28:38 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#2803
No description provided.