fix(tradein/proxy): самовосстановление пула и запасной прокси чужой affinity (#2600) #2609

Merged
lekss361 merged 2 commits from fix/tradein-proxy-pool-self-healing into main 2026-08-01 18:54:02 +00:00
Owner

Summary

  • run_proxy_healthcheck теперь перепроверяет и disabled-узлы (реже — DISABLED_RECHECK_MINUTES=60 либо если ни разу не проверялся); успешная проба реанимирует узел (enabled=true, consecutive_fails=0) и инкрементит новый счётчик revived. Без этого auto-disable по DISABLE_THRESHOLD необратим: транзиентный сбой = вечный приговор (замерено на проде — id 9/11 выключены, но физически рабочие; id 10 enabled, но 502).
  • acquire(provider) при пустой выборке по provider_affinity IN (:provider, 'any') вторым заходом берёт любой свободный здоровый прокси ЛЮБОЙ affinity (WARNING-лог), чтобы не морить источник голодом при живых свободных узлах чужой affinity. Приоритет своей affinity сохранён (тест на это есть).
  • _probe_proxy классифицирует неуспех (timeout / connect_error / http_error / other) в fail_kind, который mark_health принимает опциональным параметром — используется ТОЛЬКО для логирования. Полноценное разделение порогов (транзиент vs перманентный бан — разная скорость инкремента/disable) требует более глубокой переработки модуля (per-fail_kind счётчики) и намеренно не сделано в рамках этого PR — см. docstring mark_health в коде.

Scope

Только tradein-mvp/backend/app/services/proxy_pool.py + тесты. НЕ трогает скрейперы/browser_fetcher.py/providers/_proxy.py (доведение сигнала бана до пула — отдельная задача, п.2 issue), НЕ трогает rotate_url/ротацию IP (п.5 issue, нет ключа API), НЕ меняет provider_affinity существующих строк, без миграций.

Falsification (тесты падают на старом коде)

Было: тестовый модуль падал на ImportError: cannot import name 'DISABLED_RECHECK_MINUTES' при git stash реализации (тесты оставлены) — 0 passed / collection error.
Стало: 21 passed в tests/services/test_proxy_pool.py.

Test plan

  • tests/services/test_proxy_pool.py — 21/21 passed (offline, FakeSession)
  • Полный pytest в tradein-mvp/backend: 3018 passed, 9 skipped, 1 known pre-existing fail (test_search_api.py::test_search_cache_hit, 401 от RBAC-мидлвари, не связан с этим PR)
  • ruff check — clean на обоих файлах

Refs #2600

## Summary - `run_proxy_healthcheck` теперь перепроверяет и disabled-узлы (реже — `DISABLED_RECHECK_MINUTES=60` либо если ни разу не проверялся); успешная проба реанимирует узел (`enabled=true`, `consecutive_fails=0`) и инкрементит новый счётчик `revived`. Без этого auto-disable по `DISABLE_THRESHOLD` необратим: транзиентный сбой = вечный приговор (замерено на проде — id 9/11 выключены, но физически рабочие; id 10 enabled, но 502). - `acquire(provider)` при пустой выборке по `provider_affinity IN (:provider, 'any')` вторым заходом берёт любой свободный здоровый прокси ЛЮБОЙ affinity (WARNING-лог), чтобы не морить источник голодом при живых свободных узлах чужой affinity. Приоритет своей affinity сохранён (тест на это есть). - `_probe_proxy` классифицирует неуспех (`timeout` / `connect_error` / `http_error` / `other`) в `fail_kind`, который `mark_health` принимает опциональным параметром — используется ТОЛЬКО для логирования. Полноценное разделение порогов (транзиент vs перманентный бан — разная скорость инкремента/disable) требует более глубокой переработки модуля (per-fail_kind счётчики) и намеренно не сделано в рамках этого PR — см. docstring `mark_health` в коде. ## Scope Только `tradein-mvp/backend/app/services/proxy_pool.py` + тесты. НЕ трогает скрейперы/`browser_fetcher.py`/`providers/_proxy.py` (доведение сигнала бана до пула — отдельная задача, п.2 issue), НЕ трогает `rotate_url`/ротацию IP (п.5 issue, нет ключа API), НЕ меняет `provider_affinity` существующих строк, без миграций. ## Falsification (тесты падают на старом коде) Было: тестовый модуль падал на `ImportError: cannot import name 'DISABLED_RECHECK_MINUTES'` при `git stash` реализации (тесты оставлены) — 0 passed / collection error. Стало: `21 passed` в `tests/services/test_proxy_pool.py`. ## Test plan - [x] `tests/services/test_proxy_pool.py` — 21/21 passed (offline, FakeSession) - [x] Полный `pytest` в `tradein-mvp/backend`: 3018 passed, 9 skipped, 1 known pre-existing fail (`test_search_api.py::test_search_cache_hit`, 401 от RBAC-мидлвари, не связан с этим PR) - [x] `ruff check` — clean на обоих файлах Refs #2600
lekss361 added 1 commit 2026-08-01 05:23:38 +00:00
fix(tradein/proxy): самовосстановление пула и запасной прокси чужой affinity (#2600)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
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 2m42s
ad753c6a87
Прод-замер: disabled-узлы никогда не перепроверялись (WHERE enabled в
run_proxy_healthcheck) — auto-disable по DISABLE_THRESHOLD необратим,
транзиентный сбой = вечный приговор (id 11 сгорел за ночь, будучи
физически исправным). acquire() при пустой выборке по provider_affinity
падал в None, морив источник голодом при живых свободных узлах чужой
affinity.

- run_proxy_healthcheck: disabled-узлы проверяются реже (DISABLED_RECHECK_MINUTES=60
  либо last_check_at IS NULL); успешная проба реанимирует узел
  (enabled=true через mark_health) и инкрементит новый счётчик revived.
- mark_health(ok=True) теперь безусловно ставит enabled=true (реанимация).
- acquire: вторым заходом при пустой выборке своей affinity берёт любой
  свободный здоровый узел любой affinity (WARNING-лог), приоритет своих
  сохранён.
- _probe_proxy классифицирует неуспех (timeout/connect_error/http_error/other)
  в fail_kind — прокидывается в mark_health только для логирования; полноценное
  разделение порогов транзиент/бан отложено (см. docstring mark_health).
bot-backend added 1 commit 2026-08-01 18:36:17 +00:00
fix(tradein/proxy): не отдавать в fallback последний узел выделенной affinity (#2600)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 9s
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 2m44s
876b666424
Ревью PR #2609: domclick — ровно один узел (прод scrape_proxies.id=1),
намеренно вырезанный из общего пула через provider_affinity='domclick'
(см. 173_scrape_proxies_add_domclick_affinity.sql) — QRATOR банит всё,
кроме этого одного чистого residential-адреса. Fallback-запрос из
предыдущего коммита мог законно забрать его под avito/cian/yandex,
оставив domclick (сейчас исправно собирает: 6501 активных объявлений,
368/сутки) без прокси вообще — чинили бы один источник ценой полной
поломки другого.

- acquire(): fallback-SELECT дополнен условием "affinity='any' ИЛИ есть
  ДРУГОЙ enabled-узел той же affinity" через коррелированный EXISTS-
  подзапрос (WHERE + FOR UPDATE SKIP LOCKED + ORDER BY last_ok_at NULLS
  LAST, id — сохранены). Кандидат с единственным enabled-узлом своей
  выделенной affinity в fallback не участвует.
- Тесты: единственный domclick-узел → acquire('avito') возвращает None;
  второй enabled domclick-узел появляется — fallback снова срабатывает.
- Починен мок FakeSession (tests/services/test_proxy_pool.py):
  ветка "mark_health ok" раньше ставила enabled=True безусловно по
  совпадению общей подстроки "SET consecutive_fails = 0" (одинаковой в
  старом и новом SQL) — test_mark_health_ok_revives_disabled_proxy
  проходил бы и против кода без реанимации. Теперь ставит enabled=True
  только если в тексте SQL реально есть "enabled". Та же проблема была
  и в fallback-ветке (protects_last_node переопределял логику в Python
  независимо от SQL) — исправлено аналогично: применяется, только если
  в SQL реально есть EXISTS-подзапрос.
lekss361 merged commit 5b2d6807bc into main 2026-08-01 18:54:02 +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#2609
No description provided.