fix(tradein/proxy): учитывать историю банов при выборе egress-узла #2877

Merged
lekss361 merged 1 commit from fix/tradein-proxy-rank into main 2026-08-13 17:47:03 +00:00
Owner

Проблема (замерена на проде 2026-08-13)

Резолвер выбирал asocks-residential-1. Прямая проверка всех узлов пула на одних и тех же страницах (curl_cffi chrome120):

узел exit_ip Авито Циан
asocks-residential-1 45.86.0.32 403 403
asocks-mobile-1 5.227.31.131 429 200
asocks-mobile-2 78.37.207.246 429 200
asocks-mobile-3 94.72.28.157 429 200

То есть выбирался единственный узел, который блокируют обе площадки, при трёх рабочих.

Причина. Ранжирование шло по consecutive_fails, затем по свежести last_ok_at. У всех узлов ноль сбоев, поэтому решал healthcheck — а он проверяет доступность самого прокси, а не то, пускает ли через него целевая площадка. Узел одновременно «здоров» и заблокирован. Как только истекал TTL бана, он снова становился первым кандидатом — при ban_count = 17 по Циану.

Summary

  • NOT EXISTS заменён на LEFT JOIN scrape_proxy_source_bans — истёкшие строки банов больше не теряются, а работают как история. Проверено на проде: такие строки сохраняются вместе со счётчиком, так что сигнал есть.
  • Порядок: COALESCE(ban_count, 0) ASCconsecutive_fails ASClast_ok_at DESC NULLS LASTid. Узлы без единой записи по источнику идут первыми, дальше по возрастанию числа банов; при равенстве работает прежний tie-break.
  • Активный бан по-прежнему исключает узел полностью — эта семантика не менялась.
  • В лог выбора добавлен ban_count. Полный URL с логином и паролем по-прежнему никуда не пишется, только label/host:port/id.
  • Правило работает от данных, не от названия узла — на «mobile»/«residential» в label ничего не завязано.

Что это даёт на проде

Фактические счётчики по Авито: residential-1 — 5 банов, mobile-1 — 2, у mobile-2 и mobile-3 записей нет. Новый порядок ставит residential-1 последним вместо первого.

Для Циана он и так исключён активным баном до 16.08.

Test plan

  • Полный прогон: 4336 passed, 21 skipped
  • ruff check / ruff format — чисто, pre-commit зелёный
  • Новые тесты: узел с историей уступает чистому даже при лучших consecutive_fails/last_ok_at (репро инцидента); при равной истории работает прежний порядок; активный бан по-прежнему исключает; история по другому источнику на выбор не влияет
  • Post-deploy: убедиться, что для Авито резолвер отдаёт мобильный узел, а не residential

Известное ограничение

Ранжирование — по источникам, как и сами баны. Узел, ни разу не использованный для источника, выглядит «чистым», даже если он плох в целом: residential-1 не имеет записей по domclick и для него окажется первым. Глобальный сигнал «узел мёртв везде» ни в одной таблице не хранится — это отдельная задача, сюда не тянул.

## Проблема (замерена на проде 2026-08-13) Резолвер выбирал `asocks-residential-1`. Прямая проверка всех узлов пула на одних и тех же страницах (curl_cffi chrome120): | узел | exit_ip | Авито | Циан | |---|---|---|---| | **asocks-residential-1** | 45.86.0.32 | **403** | **403** | | asocks-mobile-1 | 5.227.31.131 | 429 | 200 | | asocks-mobile-2 | 78.37.207.246 | 429 | 200 | | asocks-mobile-3 | 94.72.28.157 | 429 | 200 | То есть выбирался единственный узел, который блокируют обе площадки, при трёх рабочих. **Причина.** Ранжирование шло по `consecutive_fails`, затем по свежести `last_ok_at`. У всех узлов ноль сбоев, поэтому решал healthcheck — а он проверяет доступность **самого прокси**, а не то, пускает ли через него целевая площадка. Узел одновременно «здоров» и заблокирован. Как только истекал TTL бана, он снова становился первым кандидатом — при `ban_count = 17` по Циану. ## Summary - `NOT EXISTS` заменён на `LEFT JOIN scrape_proxy_source_bans` — истёкшие строки банов больше не теряются, а работают как история. Проверено на проде: такие строки **сохраняются** вместе со счётчиком, так что сигнал есть. - Порядок: `COALESCE(ban_count, 0) ASC` → `consecutive_fails ASC` → `last_ok_at DESC NULLS LAST` → `id`. Узлы без единой записи по источнику идут первыми, дальше по возрастанию числа банов; при равенстве работает прежний tie-break. - **Активный бан по-прежнему исключает узел полностью** — эта семантика не менялась. - В лог выбора добавлен `ban_count`. Полный URL с логином и паролем по-прежнему никуда не пишется, только label/host:port/id. - Правило работает от данных, не от названия узла — на «mobile»/«residential» в label ничего не завязано. ## Что это даёт на проде Фактические счётчики по Авито: residential-1 — 5 банов, mobile-1 — 2, у mobile-2 и mobile-3 записей нет. Новый порядок ставит residential-1 **последним** вместо первого. Для Циана он и так исключён активным баном до 16.08. ## Test plan - [x] Полный прогон: **4336 passed, 21 skipped** - [x] `ruff check` / `ruff format` — чисто, pre-commit зелёный - [x] Новые тесты: узел с историей уступает чистому даже при лучших `consecutive_fails`/`last_ok_at` (репро инцидента); при равной истории работает прежний порядок; активный бан по-прежнему исключает; история по **другому** источнику на выбор не влияет - [ ] Post-deploy: убедиться, что для Авито резолвер отдаёт мобильный узел, а не residential ## Известное ограничение Ранжирование — по источникам, как и сами баны. Узел, ни разу не использованный для источника, выглядит «чистым», даже если он плох в целом: `residential-1` не имеет записей по domclick и для него окажется первым. Глобальный сигнал «узел мёртв везде» ни в одной таблице не хранится — это отдельная задача, сюда не тянул.
lekss361 added 1 commit 2026-08-13 17:38:13 +00:00
fix(tradein/proxy): учитывать историю банов при выборе egress-узла
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 11s
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 / backend-tests (pull_request) Successful in 4m36s
bd9c2d50bd
Замер на проде 2026-08-13: резолвер выбирал asocks-residential-1, который
отдаёт 403 и на Авито, и на Циане, при трёх рабочих мобильных узлах (Циан
через них — 200).

Причина: ранжирование шло по consecutive_fails, затем по свежести
last_ok_at. У всех узлов ноль сбоев, поэтому решала свежесть healthcheck'а,
а он проверяет доступность самого прокси, а не то, пускает ли через него
площадка. Узел одновременно «здоров» и заблокирован. Как только истекал
TTL бана, он снова становился первым кандидатом — при ban_count=17 по Циану.

- В ранжирование добавлен ban_count по этому источнику: NOT EXISTS заменён
  на LEFT JOIN, истёкшие строки банов больше не теряются, а работают как
  история. Порядок: сначала узлы без истории, дальше по возрастанию
  ban_count, при равенстве — прежний tie-break.
- Активный бан по-прежнему исключает узел полностью, семантика не менялась.
- В лог выбора добавлен ban_count — чтобы было видно, что узел с историей
  выбран осознанно. Полный URL с credentials по-прежнему не логируется.

На проде это ставит residential-1 последним для Авито (5 банов против 0 у
mobile-2/3) вместо первого.
lekss361 merged commit a4d6cbba25 into main 2026-08-13 17:47:03 +00:00
lekss361 deleted branch fix/tradein-proxy-rank 2026-08-13 17:47:04 +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#2877
No description provided.