chore(tradein/proxy): выпилить мёртвые узлы mobileproxy и их ключи из пула (#2613) #2614

Merged
lekss361 merged 1 commit from chore/tradein-purge-dead-mobileproxy into main 2026-08-01 20:05:07 +00:00
Owner

Summary

Владелец подтвердил: подписка mobileproxy закрыта, продлевать не будут. Прямая проба каждого узла из контейнера подтвердила смерть (id 2 — connection refused, id 3/4/5 — 407 Proxy Authentication Required; last_check_at у всех — 4-9 июля).

  • data/sql/201_purge_dead_mobileproxy_proxies.sqlDELETE FROM scrape_proxies WHERE url LIKE '%mobileproxy.space%'. Условие по домену в url, не по id (id разъезжается между средами, тот же приём что и в 199).
  • Убирает заодно чужой API-ключ mobileproxy, который лежал открытым текстом в rotate_url у трёх из четырёх строк (https://changeip.mobileproxy.space/?proxy_key=<секрет>) — из-за неоднородности этой колонки глубокое ревью PR #2611 нашло блокер (вызов ротации для такой строки отправил бы наш токен ASocks на чужой хост). Пин хоста в #2611 уже закрывает уязвимость, но сам чужой секрет в базе не нужен.
  • DELETE, не UPDATE: единственный FK на scrape_proxiesscrape_proxy_rotations.proxy_id (NO ACTION), таблица сейчас пуста (0 строк вообще), 0 ссылок на id 2-5. Если строки появятся до применения — DELETE упадёт по FK внутри той же транзакции (BEGIN/COMMIT), деплой завершится ошибкой без порчи данных; отдельный ON DELETE-обработчик не нужен (узлы enabled=false, acquire() их не выдаёт — новым ротациям взяться неоткуда).
  • Тест tests/test_migration_201_purge_dead_mobileproxy_proxies.py по образцу test_migration_200_*: транзакционность, domain-based (не id-based) условие, отсутствие DDL, отсутствие literal-секрета в файле, отсутствие psycopg :x::type.

Замеры (прод, read-only, перед миграцией)

  1. Строк под условие: 4 (id 2 ha-507932, id 3 gi-501114, id 4 ard-500825, id 5 aup-501129).
  2. Остаток пула после: 4 — все ASocks (id 1 asocks-residential-1, id 9 asocks-mobile-1, id 10 asocks-mobile-2, id 11 asocks-mobile-3).
  3. Строк в scrape_proxy_rotations на id 2-5: 0 (таблица пуста целиком, 0 строк всего).
  4. Секрет-паттерн (token|bearer|secret|key=|password, regex, case-insensitive) в rotate_url остающихся 4 строк: 0 совпадений — все четыре https://api.asocks.com/unlimited-proxy/<portId>/refresh-ip, без query-параметров, авторизация Bearer-заголовком вне URL.
  5. FK на scrape_proxies: ровно один — scrape_proxy_rotations.proxy_id -> scrape_proxies.id, delete_rule = NO ACTION.

Границы

  • НЕ трогает ASocks-строки (id 1, 9, 10, 11) и их rotate_url.
  • НЕ трогает переменные окружения / .env.runtime — отдельная задача, и НЕ раньше неё (при пустом прокси curl_proxy_url отдаёт None = прямое подключение с IP сервера).
  • НЕ трогает app/services/proxy_pool.py, proxy_rotation.py, скраперы.
  • Номер миграции (201) перепроверен по forgejo/main (max 200), всем открытым PR (#2545/#2546/#2547 — не в data/sql) и по истории коммитов за файлами */data/sql/20*.sql во всех remote-ветках — коллизий нет.

Test plan

  • uv run pytest -q в tradein-mvp/backend: 3056 passed, 1 pre-existing fail (tests/test_search_api.py::test_search_cache_hit, 401 RBAC — не связан с этим PR, не чинится), 9 skipped.
  • ruff check / ruff format — чисто (pre-commit hooks прошли).

Refs #2613

## Summary Владелец подтвердил: подписка mobileproxy закрыта, продлевать не будут. Прямая проба каждого узла из контейнера подтвердила смерть (id 2 — connection refused, id 3/4/5 — 407 Proxy Authentication Required; last_check_at у всех — 4-9 июля). - `data/sql/201_purge_dead_mobileproxy_proxies.sql` — `DELETE FROM scrape_proxies WHERE url LIKE '%mobileproxy.space%'`. Условие по домену в `url`, не по `id` (id разъезжается между средами, тот же приём что и в 199). - Убирает заодно чужой API-ключ mobileproxy, который лежал открытым текстом в `rotate_url` у трёх из четырёх строк (`https://changeip.mobileproxy.space/?proxy_key=<секрет>`) — из-за неоднородности этой колонки глубокое ревью PR #2611 нашло блокер (вызов ротации для такой строки отправил бы наш токен ASocks на чужой хост). Пин хоста в #2611 уже закрывает уязвимость, но сам чужой секрет в базе не нужен. - `DELETE`, не `UPDATE`: единственный FK на `scrape_proxies` — `scrape_proxy_rotations.proxy_id` (`NO ACTION`), таблица сейчас пуста (0 строк вообще), 0 ссылок на id 2-5. Если строки появятся до применения — `DELETE` упадёт по FK внутри той же транзакции (`BEGIN/COMMIT`), деплой завершится ошибкой без порчи данных; отдельный `ON DELETE`-обработчик не нужен (узлы `enabled=false`, `acquire()` их не выдаёт — новым ротациям взяться неоткуда). - Тест `tests/test_migration_201_purge_dead_mobileproxy_proxies.py` по образцу `test_migration_200_*`: транзакционность, domain-based (не id-based) условие, отсутствие DDL, отсутствие literal-секрета в файле, отсутствие psycopg `:x::type`. ## Замеры (прод, read-only, перед миграцией) 1. Строк под условие: **4** (id 2 `ha-507932`, id 3 `gi-501114`, id 4 `ard-500825`, id 5 `aup-501129`). 2. Остаток пула после: **4** — все ASocks (id 1 `asocks-residential-1`, id 9 `asocks-mobile-1`, id 10 `asocks-mobile-2`, id 11 `asocks-mobile-3`). 3. Строк в `scrape_proxy_rotations` на id 2-5: **0** (таблица пуста целиком, 0 строк всего). 4. Секрет-паттерн (`token|bearer|secret|key=|password`, regex, case-insensitive) в `rotate_url` остающихся 4 строк: **0 совпадений** — все четыре `https://api.asocks.com/unlimited-proxy/<portId>/refresh-ip`, без query-параметров, авторизация Bearer-заголовком вне URL. 5. FK на `scrape_proxies`: ровно один — `scrape_proxy_rotations.proxy_id -> scrape_proxies.id`, `delete_rule = NO ACTION`. ## Границы - НЕ трогает ASocks-строки (id 1, 9, 10, 11) и их `rotate_url`. - НЕ трогает переменные окружения / `.env.runtime` — отдельная задача, и НЕ раньше неё (при пустом прокси `curl_proxy_url` отдаёт `None` = прямое подключение с IP сервера). - НЕ трогает `app/services/proxy_pool.py`, `proxy_rotation.py`, скраперы. - Номер миграции (201) перепроверен по `forgejo/main` (max 200), всем открытым PR (#2545/#2546/#2547 — не в `data/sql`) и по истории коммитов за файлами `*/data/sql/20*.sql` во всех remote-ветках — коллизий нет. ## Test plan - [x] `uv run pytest -q` в `tradein-mvp/backend`: 3056 passed, 1 pre-existing fail (`tests/test_search_api.py::test_search_cache_hit`, 401 RBAC — не связан с этим PR, не чинится), 9 skipped. - [x] `ruff check` / `ruff format` — чисто (pre-commit hooks прошли). Refs #2613
lekss361 added 1 commit 2026-08-01 19:54:30 +00:00
chore(tradein/proxy): выпилить мёртвые узлы mobileproxy и их ключи из пула (#2613)
All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / 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 2m40s
779b57e1b3
lekss361 merged commit 16237573ed into main 2026-08-01 20:05:07 +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#2614
No description provided.