feat(tradein/proxy): прогон знает свой узел, а снятый бан перестаёт стирать историю (#3404) #3405
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3405
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/3404-proxy-run-attribution"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Закрывает #3404.
Зачем
Выбор оператора мобильного прокси (МегаФон / Tele2 / МТС / Билайн) опирался на две ненадёжные опоры.
proxy_idбыл только уscrape_proxy_source_bansиscrape_proxy_rotations. Вопрос «через какой узел шёл прогон, обогативший 5 карточек из 21» не выяснялся ни одним запросом.clear_source_bansделалаDELETE, а зовётся она после КАЖДОЙ успешной ротации exit-IP (proxy_rotation.py:469и:601). У #540723 (МегаФон) 23 успешные ротации с 31.08 — и ноль строк банов; у #540722 (Tele2) ротаций почти не было — и 7 банов. Сравнение «7 против 0» читалось как «Tele2 хуже», хотя в той же мере это «у МегаФона историю стёрли 23 раза».На этих числах прямо сейчас принимается решение о деньгах: какие порты пересаживать на другого оператора и сколько докупать.
Что сделано
A.
scrape_runs.proxy_id— последний выданный прогону узел; полная цепочка (если узел менялся mid-run) копится вcounters.proxy_ids. Пишетproxy_pool.attribute_run_proxyиз единственной точки — сразу после выдачи лиза вacquire(), поэтому curl-путь (лиз на каждый вызов), браузерный sticky lease и ре-acquire при ротации покрыты одинаково, без правки сигнатур провайдеров.run_idдоходит до адаптера через ContextVar (scraper_kit.orchestration.run_context): протоколProxyProvider.acquire(provider)его не несёт, аRealProxyProvider— один объект на весь планировщик.create_runвыставляет, финализаторы сбрасывают. Если прогон иacquireокажутся в разных тредах, созданных доset— атрибуция молча останется NULL, это не ошибка.Best-effort по построению:
SET LOCAL lock_timeout = '2s'и проглоченное исключение. Строку прогона параллельно пишет heartbeat из другой сессии, а путь выдачи прокси ждать на блокировке не может; лиз к этому моменту уже закоммичен.B.
clear_source_bansгасит строку, а не удаляет:banned_until = now(),ban_count = 0,cleared_at/cleared_reason. Строка доживает до штатного purge поSOURCE_BAN_PURGE_DAYS.Что НЕ меняется
Эскалация сохранена 1:1. Формула в
mark_bannedберёт ПРЕДЫДУЩИЙban_countпоказателем степени (base * 2^ban_count), поэтому погашенная строка с нулём даёт ровноSOURCE_BAN_BASE_HOURS= 6ч — байт-в-байт как свежий INSERT после DELETE. Отдельный тест ровно на это: забанить → снять → забанить снова → срок базовый, а не удвоенный.Погашенная строка неотличима от отсутствующей для всех читателей — проверены поимённо:
acquire(основной и fallback-запрос), оба guard-подзапросаmark_banned, holder-запрос диагноза,proxy_egress._pick_candidate(ранжирование поCOALESCE(b.ban_count, 0)даёт 0, как у узла без истории),admin._active_source_bans. Все гейтятся поbanned_until > now(), ни один не считает «строка есть» синонимом «забанен».Ничего не бэкфиллится. Связать прошедшие прогоны с узлами нечем:
leased_byисторически =NON_RUN_LEASE_MARKER, лог выдачи лизов run_id не хранит. Для строк до миграции 287proxy_idостаётся NULL — это «атрибуция не собиралась», а не «прогон без прокси».Миграция
287_proxy_run_attribution.sql— идемпотентна (ADD COLUMN IF NOT EXISTS×3,CREATE INDEX IF NOT EXISTS), одна транзакция,lock_timeout5с, DML нет вовсе.ON DELETE SET NULLу FK: вывод узла из пула не должен уносить историю прогонов.Тесты
9 новых (
test_3404_proxy_run_attribution.py) + 14 существующих переведены с DELETE-семантики на гашение. В тестах про утечку секретов добавлена проверка НОВОГО поля: ключ ротации не должен попадать и вcleared_reason— раньше это поле не проверялось, потому что его не было.Полный прогон бэкенда после мержа свежего main: 5606 passed, 37 skipped.
Чего этот PR не делает
Не лечит сам дефицит ёмкости пула. У Авито сейчас один пригодный узел, за 7 дней 34 из 47 отказов
avito_detail_backfillпомеченыban_kind=infra(сайдкар либо пустой пул), и в логах естьNS_ERROR_PROXY_TOO_MANY_REQUESTS— 429 от самого провайдера. Это отдельная работа; здесь только измеримость выбора.🤖 Generated with Claude Code