fix(tradein/proxy): упавшая проба присваивала себе бан боевого сбора (#2800) #2805
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#2805
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/probe-must-not-steal-ban"
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?
Что сломано
Прод, замер 09.08.2026 — пара
(proxy 1, cian):banned_at) — боевой сборbanned:cianupdated_at) — упавшая пробаprobe:browsermark_bannedвON CONFLICT (proxy_id, source) DO UPDATEпереписывалreasonбезусловно, поэтому проба забирала чужую строку себе — вместе с эскалацией: отдых пары вырос с 6 ч до 12 ч на счётчике, который проба не заработала.Фильтр «снимаю только своё» (
clear_source_bans(only_reason=...)) сделан правильно и работает: удалить чужую строку проба не может. Но присвоить может — и тогда фильтр перестаёт защищать: строка уже «своя», и следующий зелёный robots.txt снимет ею бан, поставленный боевым сбором по настоящему отказу площадки. Плюс метка перестаёт быть свидетельством:banned:cian(«площадка нас отбила») иprobe:browser(«наша проба не смогла») — разные факты с разными последствиями, ровно ловушка #2764.Правило
В
WHEREуDO UPDATE: строку берём, только если она истекла (живого владельца нет), либо она уже наша, либо мы боевой сбор. Иначе не трогаем ничего — ниreason, ниban_count, ни срок.Вариант «продлить срок, это же безвредно» отвергнут: срок пересчитывается от
now()по НАШЕЙ эскалации и способен укоротить уже эскалированный чужой бан. Бан и так стоит — делать нечего.Зеркальный случай: да, боевой сбор строку пробы перехватывает — намеренно
Его вердикт сильнее (площадка отбила именно сейчас): пара остаётся забаненной, а метка становится точнее. Запретить и ему — значит оставить строку за пробой, и её же успешный robots.txt снесёт настоящий бан площадки: тот же дефект, только зеркально и хуже. Асимметрия закреплена тестом
test_live_ban_takes_over_the_probe_row.Цена перехвата —
ban_countнаследуется (отдых чуть длиннее заслуженного). Обнулять его на смене владельца нельзя: тогда запись пробы стирала бы память об эскалации боевых банов пары.Красный прогон на текущем коде
Новые тесты приложены к чистому
origin/main(worktree на9cd6db02) без правки сервиса:Сценарий взят с прода дословно: активная
banned:cian, ban_count=1, затем упавшая проба (page, заглушка Циана вместо robots.txt). После фикса —reasonосталсяbanned:cian,ban_count= 1, срок не пересчитан,pair_banned= 0. МокFakeSessionгейтит защиту по подстроке боевого SQL (как и остальные ban-предикаты в нём) — иначе тест был бы зелёным на сломанном коде.Второй тест из задачи — «успешная проба не снимает чужой бан» — уже существовал (
test_probe_clears_only_its_own_ban, #2803); добавлено, что чужая строка не только цела, но и не переписана (reason/ban_count).Синтаксис самого UPSERT проверен парсером PostgreSQL (
pglast.parse_sql) — локального Postgres в контуре нет, юнит-тесты идут черезFakeSession.Диагностика
0 rows у апсерта теперь означает три разные вещи — чужой активный владелец / защита последнего узла / нет узла.
mark_bannedвозвращает исход строкой (banned|deferred|protected|missing) и пишет отдельный лог на каждый, а счётчикpair_bannedбольше не объявляет баном то, чего не записал. Раньше «не перебил чужой бан» было бы залогировано как «это последний узел» — ложный диагноз, который читался бы дальше как факт.Прод: сколько строк затронуто и что с ними будет
Сейчас одна строка с перехваченным происхождением —
(1, cian),probe:browser, до 10.08 07:43. Всего активных строк 4, probe-owned из них 1.Честная оговорка: из самой таблицы перехват в общем случае не доказуем — повторный отказ пробы даёт такую же картину (
probe:browser,ban_count>1). Про эту строку известно точно, потому чтоbanned_at(18:21, вставка боевого сбора) не совпадает сupdated_at(19:43, запись пробы) при probe-метке, и потому что предыдущее состояние строки видел автор #2803.Что с ней станет: сама не «дозреет» до 07:43. Строка теперь носит метку пробы, поэтому первый же зелёный robots.txt по паре (1, cian) её удалит — фикс не восстанавливает украденное происхождение задним числом. Практический риск мал: если Циан всё ещё отбивает этот узел, боевой сбор запишет бан заново (уже своей меткой, и с этого момента проба его не тронет). Отдельный шаг руками возможен, но это решение владельца, не моё — правка была бы
UPDATE scrape_proxy_source_bans SET reason='banned:cian', ban_count=1, banned_until=banned_at + interval '6 hours' WHERE proxy_id=1 AND source='cian'; я к проду только на чтение.Про мигающую пару
Ограничение из #2803 (17:55 отказ → 19:44 успех → 19:50 снова отказ) правка не усугубляет: поведение «успешная проба снимает СВОЙ бан» не тронуто, а случай «успешная проба гасит бан боевого сбора» теперь невозможен в принципе. Преждевременное снятие на мигающей паре остаётся открытым — оно про порог подтверждения успеха, не про владельца строки.
Остаточное (не чиню здесь)
Истёкшую чужую строку проба по-прежнему занимает вместе с её
ban_count(это необходимо: иначе проба не смогла бы записать вердикт по паре, которую когда-либо банил сбор). Последствие — её успех может удалить строку с накопленной памятью об эскалации боевых банов. Строка к тому моменту уже истёкшая, и purge всё равно снёс бы её черезSOURCE_BAN_PURGE_DAYS.Test plan
pytest tests/test_2800_per_source_probe.py tests/services/test_proxy_pool.py— 72 passedorigin/main— 1 failed (см. выше)бан пары уже стоит от 'banned:...', аreasonвscrape_proxy_source_bansбольше не меняется наprobe:browserRefs #2800