feat(tradein/deactivate-stale): страховочные рельсы объёма снятия (PR-B) #3066
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#3066
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/deactivate-stale-safety-rails"
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?
Продолжение #2659 после PR-A (#3056).
Что было
У джобы деактивации не было ни одного ограничителя ОБЪЁМА снятия.
min_confirmationsможет пропустить прогон целиком, пол переобхода поднимает TTL,cap_multограничивает сам пол — но ни один не смотрит на размер выборки, из которойpercentile_discпосчитал квантиль, и ни один не смотрит на то, сколько строк снимет UPDATE. UPDATE идёт одним statement'ом без LIMIT: при обвале любого гейта выше снимается сколько снимется.Рельс 1 — гейт деградации пола
floor_n_pairsнижеmin_floor_pairs(30) либо упал более чем вfloor_drop_ratio(5) раз относительно предыдущего успешного прогона того же расписания →skipped_floor_degraded, ни одна строка не тронута.Почему самокалибрующийся, а не ручной порог: у
floor_n_pairsистории почти нет (счётчик из PR-A), поэтому главный сигнал — относительный, «прогон сам с собой». Абсолютный порог — страховка на случай, когда истории ещё нет или она сама уже вырождена (иначе относительный сравнивал бы вырожденное с вырожденным и молчал бы вечно).Сравнение идёт по
scrape_runs.source, не поlisting_source: уdeactivate_stale_cianиdeactivate_stale_cian_null_segmentlisting_sourceодин и тот же ('cian'), но это две независимые серии с несопоставимым масштабом выборки — сравнивать их друг с другом было бы категориальной ошибкой.floor_drop_ratio=5заимствован по порядку величины у соседнего, уже проверенного на реальном инциденте гейта: здоровый разброс avito по суткам —confirmations2542..6079, то есть ~2.4×. Штатный шум заведомо не достигает 5×, а провал 10.07-26.07 (падение в 5-10+ раз) ловится.Рельс 2 — аварийный потолок объёма
max_deactivated=15000. Preflightcount(*)по тому же предикату, что исполнит UPDATE → abort до записи.Это не рабочий порог, а предохранитель последней инстанции: исторический максимум легитимного снятия — 9 300 (avito 06.06), далее 6 531 / 6 131 / 4 959 / 3 909 (последнее owner явно подтвердил как здоровую чистку). Запас 1.6× над максимумом, но катастрофу на порядок крупнее ловит.
Порога по ДОЛЕ пула сознательно нет. Пул, с которым эта джоба реально работает, никогда не записывался: восстановить постфактум можно только по
listing_sources, аdeactivatedсчитает строкиlistings— разная гранулярность. Проверено на историческом ряду: доля деактивированного от восстановленного пула — 40 %, 97 %, 317 %, 1652 %. Ряд не осмысленный, калибровать не на чем. Поэтомуdeactivation_candidates,active_pool,deactivated_pctпока только пишутся; долевой порог — когда накопится собственная история.Область действия
Оба рельса живут внутри
revisit_floor_quantile > 0. Проверено на проде:revisit_floor_quantile0явноТо есть null-сегментные джобы (чей пул на 2-3 порядка меньше потолка) гейт не затрагивает — это подтверждено конфигом, а не только докстрингом.
Валидация параметров
min_floor_pairs/floor_drop_ratio/max_deactivatedприходят изdefault_paramsрасписания, то есть из jsonb. Отбивается тот же класс опечатки, что уttl_days/cap_mult:boolпроверяется до числового сравнения (иначеTrue < 30тихо прошло бы как1 < 30).Test plan
249 passed, 1 skipped(-k "deactivate or stale")test_deactivate_stale_floor_degradation.py,test_deactivate_stale_deactivation_cap.py— включаяtest_candidates_predicate_matches_update_predicate, который стережёт синхронность preflight-предиката с UPDATE (предикат продублирован текстуально: рефакторинг уже протестированных UPDATE-builder'ов вне скоупа)CAST(:x AS type),:x::typeв диффе нетfloor_n_pairsна прогоне 24.08 утром — до мержа (см. блок вверху)Побочное
uv.lock— две строкиrequires-distподтянуты кpyproject(curl-cffi >=0.7.0→>=0.15.0). Устранение устаревшей записи, не смена зависимости: разрешённые версии не изменились, лок расходился с pyproject уже на main и регенерировался при любом вызовеuv.Refs #2659
Замер состоялся — снимаю стоп. Порог безопасен с запасом в 21–71 раз.
Первые реальные
floor_n_pairs(прогоны 24.08 утром)floor_n_pairsconfirmationsrevisit_floor_daysdeactivate_stale_yandexdeactivate_stale_ciandeactivate_stale_avitodeactivate_stale_domklikdeactivate_stale_yandex_null_segmentdeactivate_stale_cian_null_segmentВсе шесть прогонов
done.Что это подтверждает
1.
min_floor_pairs=30никого не заморозит. Самое низкое реальное значение — domklik 625, это в 21 раз выше порога. Опасение из шапки PR («если у источника здоровое значение окажется 12, гейт заморозит его деактивацию, и выглядеть это будет как „гейт отработал“») не подтвердилось. Мержу как есть, без правки константы.2. Область действия совпала с предсказанной по конфигу. У обоих null-сегментных расписаний
floor_n_pairsпуст — пол у них выключен (revisit_floor_quantile: 0вdefault_params), значит гейт их не касается. Это ровно то, что я записал в таблицу «область действия» до замера, и теперь оно подтверждено фактом, а не чтением конфига.3.
floor_drop_ratio=5тоже выглядит разумно на этих числах. Падение впятеро означало бы, например, domklik с 625 до менее чем 125 — это обвал, а не суточный шум. Соотношениеfloor_n_pairsкconfirmationsпри этом стабильное и осмысленное: 0.44 (avito), 0.80 (domklik), 0.92 (yandex/cian) — то есть счётчик считает подмножество, как и задумано, а не случайную величину.Оговорка
Это один срез, а не ряд. Порог
min_floor_pairsтеперь опирается на четыре реальных наблюдения вместо нуля — этого достаточно, чтобы не заморозить источник сегодня, но недостаточно, чтобы считать 30 откалиброванным числом. Относительный гейт (floor_drop_ratio) остаётся главным механизмом именно поэтому: он сравнивает прогон сам с собой и не требует чужой калибровки.Через неделю стоит пересмотреть обе константы уже по накопленному ряду — как и написано в комментарии у самих констант.
CI зелёный (8/8),
mergeable=true. Мержу.Первые прогоны с рельсами в проде (25.08). Механизм работает целиком, ложных срабатываний нет.
Что показали живые прогоны
floor_n_pairs..._previousskipped_floor_degradedskipped_cap_exceededdeactivated_pctdeactivate_stale_avitodeactivate_stale_domklikdeactivate_stale_yandex_null_segmentdeactivate_stale_cianВсе
done. Два оставшихся расписания (yandex,cian_null_segment) ещё не отработали на момент замера.Главное: относительный гейт получил базу сравнения
floor_n_pairs_previousреально читается и записывается — 747, 625, 1464. Это был самый хрупкий узел всей правки:_PREVIOUS_FLOOR_N_PAIRS_SQLищет предыдущий успешный прогон того же расписания поscrape_runs.source, и если бы запрос не находил строку (не тот ключ, не тот статус,counters ->>не тот тип), гейт молча деградировал бы до одного лишь абсолютного порога — и никто бы не заметил, потому что абсолютный порог сейчас не срабатывает.Значения совпадают со вчерашним замером один в один (747 → вчерашний avito, 625 → domklik, 1464 → cian). То есть подхватывается именно предыдущий прогон, а не случайная строка.
Ложных срабатываний нет
skipped_floor_degradedиskipped_cap_exceededпусты во всех прогонах. Это ожидаемо и правильно:Рост объясним: вчерашние прогоны шли на меньшей базе, плюс
domclick_city_sweepза ночь добавил листингов (у domklik активных стало 2899 против 1351).Наблюдательные счётчики тоже пишутся
deactivated_pct= 1 у cian при 281 снятых — то естьdeactivation_candidates/active_poolсчитаются и preflight отрабатывает. Это те метрики, по которым позже можно будет откалибровать долевой порог, которого сейчас сознательно нет.Первая точка ряда
В шапке PR я оговорил, что один срез — не ряд, и что через неделю константы стоит пересмотреть. Теперь точек две, и они уже дают полезное:
Суточный разброс до +42 % — это и есть тот шум, относительно которого выбран порог падения ×5. Пока данные подтверждают, что запас выбран разумно: даже сорокапроцентные колебания далеки от пятикратного обвала. Но нужен ряд подлиннее, прежде чем считать это доказанным — колебание вверх и обвал вниз не обязаны быть симметричны.
Refs #2659