fix(tradein/deactivate): TTL не снимает объявления по порогу ниже собственного цикла обхода #2797

Merged
bot-backend merged 2 commits from fix/2659-revisit-floor into main 2026-08-09 17:26:19 +00:00
Collaborator

Что происходит

Гейт здоровья #2710 отвечает на вопрос «источник вообще собирается?» и делает это
верно — Домклик на проде честно пропускается третьи сутки подряд. Но он не отвечает
на второй вопрос #2659: достаточно ли ttl_days, чтобы молчание означало снятие?
Пока свип возвращается к строке реже, чем раз в ttl_days, TTL меряет НАШУ выборку,
а не жизнь объявления, — и источник при этом полностью здоров, так что гейт молчит.

Замер до правки (прод, read-only, 2026-08-09)

С деплоя гейта 06.08 TTL снял 1 028 строк, 127 из них (12.4%) уже снова активны
свип нашёл их живыми через 1-3 суток и вернул сам (upsert в scraper_kit/base.py
ставит is_active = true). В разбивке по городам видно, что это не шум:

срез снято снова активны
cian / Екатеринбург 103 103 (100%)
yandex / Екатеринбург 24 24 (100%)
cian / без города 560 0
yandex / без города 341 0

В единственном городе с настоящим покрытием ложны все снятия. Возраст на момент
снятия у всех 127 — 29.9..30.3 суток при TTL=30: срабатывание ровно на границе, а
свип возвращался к строке на 31-34-е сутки.

Почему это не лечится новой константой

Разрывы переобхода против текущих TTL (listing_source_snapshots, 40 суток, срез
совпадает со срезом TTL-джобы):

источник / сегмент p90 p99 TTL сейчас TTL/p99
domklik / vtorichka 1.9 3.1 14 4.5 ← контрольная группа
cian / vtorichka 10.9 26.6 30 1.13
yandex / vtorichka 5.7 43.0 30 0.70
avito / vtorichka 29.1 42.1 10 0.24 ← отсюда 9 033 строки

Домклик — контроль из своих же данных: при почти полном суточном обходе TTL лежит в
4.5 раза выше хвоста, и снятие у него действительно означает снятие. У остальных трёх
порог сидит вплотную к хвосту или внутри него. Руками подобранное число и есть корень
#2659 — чинить его вторым руками подобранным числом бессмысленно.

Что сделано

Вместо константы меряем факт: какой самый большой возраст, при котором свип за
последнее окно ДОКАЗАЛ строку живой (нашёл её на площадке). Если свип только что
вернул к жизни строку, молчавшую 40 суток, то 30 суток молчания не доказывают ничего.

эффективный TTL = max(ttl_days, пол) — пол только поднимает порог, никогда не
опускает. Считается по ТОМУ ЖЕ срезу source+segments и по ТОЙ ЖЕ колонке свежести,
что и сам UPDATE, ДО любой записи. Нет истории снимков → пола нет, TTL как задан.

Побочный эффект намеренный: после провала сбора хвост разрывов распухает (свип
разгребает завал), пол растёт, деактивация замирает сама — ровно то, чего issue просил
от «гейта по банам», но выраженное через результат, а не через причину. Когда завал
разобран, хвост схлопывается и пол опускается обратно.

Миграции нет: новых пороговых констант не появилось, в этом и смысл правки. Квантиль
0.99 — калибровочная ручка в default_params расписания (revisit_floor_quantile,
0 → пол выключен), подобран по требованию «пол обязан накрыть возраст 30.3 доказанно
ложных снятий».

Видимое пользователю изменение числа объявлений

Ближайший прогон Яндекса перестаёт снимать 43 активные строки vtorichka. На проде
это единственные строки, которых TTL сейчас вообще касается (avito, cian, domklik →
0 строк под ножом на сегодня). Дальше эффект идёт потоком: ~100-300 снятий в сутки,
из которых доказанно ложных 12.4%.

Test plan

  • test_effective_ttl_covers_every_proven_false_kill — проигрывает 127 доказанных
    ложных снятий на всех трёх прод-срезах; на старом коде 13 из 17 тестов красные
    (эффективный TTL остаётся 30 и не накрывает возраст 30.3)
  • контрольная группа зафиксирована тестом: домклику пол не нужен (TTL/p99 = 4.5),
    остальным трём нужен (0.24..1.13) — если это перестанет быть так, тест упадёт
  • пол не опускает TTL, округляется вверх, считается ДО UPDATE, тот же срез и та же
    колонка свежести, whitelist колонки, гейт здоровья выигрывает у пола
  • ruff check + ruff format чисто; 105 тестов семейства deactivate_stale зелёные
  • SQL прогнан на проде read-only ровно в том виде, в каком его строит код:
    cian/vtorichka 33.99, yandex/vtorichka 74.25, avito 69.67, domklik NULL; 235 мс

Прод-верификация (критерий записан ДО факта)

Прогон deactivate_stale_yandex ближайшим утром после деплоя должен дать
counters с ttl_days_effective >= 34 и deactivated = 0 вместо ожидаемых 43,
а в логах — warning «TTL поднят с 30 до N сут». Если deactivated снова окажется
43 — правка не доехала, а не «сработала тихо».

Refs #2659

## Что происходит Гейт здоровья #2710 отвечает на вопрос «источник вообще собирается?» и делает это верно — Домклик на проде честно пропускается третьи сутки подряд. Но он не отвечает на второй вопрос #2659: **достаточно ли `ttl_days`, чтобы молчание означало снятие?** Пока свип возвращается к строке реже, чем раз в `ttl_days`, TTL меряет НАШУ выборку, а не жизнь объявления, — и источник при этом полностью здоров, так что гейт молчит. ## Замер до правки (прод, read-only, 2026-08-09) С деплоя гейта 06.08 TTL снял **1 028 строк, 127 из них (12.4%) уже снова активны** — свип нашёл их живыми через 1-3 суток и вернул сам (upsert в `scraper_kit/base.py` ставит `is_active = true`). В разбивке по городам видно, что это не шум: | срез | снято | снова активны | |---|---|---| | cian / Екатеринбург | 103 | **103 (100%)** | | yandex / Екатеринбург | 24 | **24 (100%)** | | cian / без города | 560 | 0 | | yandex / без города | 341 | 0 | В единственном городе с настоящим покрытием ложны **все** снятия. Возраст на момент снятия у всех 127 — 29.9..30.3 суток при TTL=30: срабатывание ровно на границе, а свип возвращался к строке на 31-34-е сутки. ## Почему это не лечится новой константой Разрывы переобхода против текущих TTL (`listing_source_snapshots`, 40 суток, срез совпадает со срезом TTL-джобы): | источник / сегмент | p90 | p99 | TTL сейчас | TTL/p99 | |---|---|---|---|---| | **domklik / vtorichka** | 1.9 | 3.1 | 14 | **4.5** ← контрольная группа | | cian / vtorichka | 10.9 | 26.6 | 30 | 1.13 | | yandex / vtorichka | 5.7 | 43.0 | 30 | 0.70 | | avito / vtorichka | 29.1 | 42.1 | 10 | **0.24** ← отсюда 9 033 строки | Домклик — контроль из своих же данных: при почти полном суточном обходе TTL лежит в 4.5 раза выше хвоста, и снятие у него действительно означает снятие. У остальных трёх порог сидит вплотную к хвосту или внутри него. Руками подобранное число и есть корень #2659 — чинить его вторым руками подобранным числом бессмысленно. ## Что сделано Вместо константы меряем **факт**: какой самый большой возраст, при котором свип за последнее окно ДОКАЗАЛ строку живой (нашёл её на площадке). Если свип только что вернул к жизни строку, молчавшую 40 суток, то 30 суток молчания не доказывают ничего. `эффективный TTL = max(ttl_days, пол)` — пол только поднимает порог, никогда не опускает. Считается по ТОМУ ЖЕ срезу `source+segments` и по ТОЙ ЖЕ колонке свежести, что и сам UPDATE, ДО любой записи. Нет истории снимков → пола нет, TTL как задан. Побочный эффект намеренный: после провала сбора хвост разрывов распухает (свип разгребает завал), пол растёт, деактивация замирает сама — ровно то, чего issue просил от «гейта по банам», но выраженное через результат, а не через причину. Когда завал разобран, хвост схлопывается и пол опускается обратно. Миграции нет: новых пороговых констант не появилось, в этом и смысл правки. Квантиль 0.99 — калибровочная ручка в `default_params` расписания (`revisit_floor_quantile`, 0 → пол выключен), подобран по требованию «пол обязан накрыть возраст 30.3 доказанно ложных снятий». ## Видимое пользователю изменение числа объявлений **Ближайший прогон Яндекса перестаёт снимать 43 активные строки vtorichka.** На проде это единственные строки, которых TTL сейчас вообще касается (avito, cian, domklik → 0 строк под ножом на сегодня). Дальше эффект идёт потоком: ~100-300 снятий в сутки, из которых доказанно ложных 12.4%. ## Test plan - [x] `test_effective_ttl_covers_every_proven_false_kill` — проигрывает 127 доказанных ложных снятий на всех трёх прод-срезах; **на старом коде 13 из 17 тестов красные** (эффективный TTL остаётся 30 и не накрывает возраст 30.3) - [x] контрольная группа зафиксирована тестом: домклику пол не нужен (TTL/p99 = 4.5), остальным трём нужен (0.24..1.13) — если это перестанет быть так, тест упадёт - [x] пол не опускает TTL, округляется вверх, считается ДО UPDATE, тот же срез и та же колонка свежести, whitelist колонки, гейт здоровья выигрывает у пола - [x] `ruff check` + `ruff format` чисто; 105 тестов семейства deactivate_stale зелёные - [x] SQL прогнан на проде read-only ровно в том виде, в каком его строит код: cian/vtorichka 33.99, yandex/vtorichka 74.25, avito 69.67, domklik NULL; 235 мс ## Прод-верификация (критерий записан ДО факта) Прогон `deactivate_stale_yandex` ближайшим утром после деплоя должен дать `counters` с `ttl_days_effective >= 34` и `deactivated = 0` вместо ожидаемых 43, а в логах — warning «TTL поднят с 30 до N сут». Если `deactivated` снова окажется 43 — правка не доехала, а не «сработала тихо». Refs #2659
bot-backend added 1 commit 2026-08-09 17:14:52 +00:00
fix(tradein/deactivate): TTL не снимает объявления по порогу ниже собственного цикла обхода
All checks were successful
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 3m58s
8be6973255
Гейт #2710 отвечает «источник собирается?». Он не отвечает на второй вопрос
#2659 — «а достаточно ли ttl_days, чтобы молчание означало снятие?». Пока свип
возвращается к строке реже, чем раз в ttl_days, TTL меряет нашу выборку, а не
жизнь объявления, — и источник при этом полностью здоров, так что гейт молчит.

Замер на проде 2026-08-09 (read-only): с деплоя гейта 06.08 TTL снял 1 028 строк,
127 из них (12.4%) уже снова активны — свип нашёл их живыми через 1-3 суток и
вернул сам. В Екатеринбурге, единственном городе с настоящим покрытием, доля
ложных снятий 100% (cian 103/103, yandex 24/24); возраст на момент снятия у всех
127 — 29.9..30.3 суток при TTL=30, то есть срабатывание ровно на границе.

Корень не в конкретном числе, а в том, что число подобрано руками ниже хвоста
обхода. Разрывы переобхода против текущих TTL (listing_source_snapshots, 40 сут):
domklik 3.1 при TTL=14 (4.5x запас, сплошное суточное покрытие — контрольная
группа), cian 26.6 при 30, yandex 43.0 при 30, avito 42.1 при 10. Поэтому второе
подобранное руками число проблему не решает.

Вместо константы меряем факт: какой самый большой возраст, при котором свип за
последнее окно ДОКАЗАЛ строку живой. Эффективный TTL = max(ttl_days, этот пол),
по тому же срезу source+segments и по той же колонке свежести, что и UPDATE.
Пол только поднимает порог. Побочный эффект намеренный: после провала сбора хвост
разрывов распухает, пол растёт, деактивация замирает сама — то, чего issue просил
от «гейта по банам», но выраженное через результат, а не через причину.

Тот же запрос на проде даёт cian/vtorichka 34.0, yandex/vtorichka 74.3,
avito 69.7, domklik NULL (сбор стоит, гейт его и так пропускает) — 235 мс,
раз в сутки. Квантиль 0.99 — калибровочная ручка в default_params расписания,
подобран по требованию «пол обязан накрыть возраст 30.3 доказанно ложных снятий».

Видимое пользователю: ближайший прогон Яндекса перестаёт снимать 43 активные
строки vtorichka; на проде это единственные строки, которых TTL сейчас касается.

Refs #2659
Light1YT added 1 commit 2026-08-09 17:20:23 +00:00
Merge branch 'main' into fix/2659-revisit-floor
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 3m53s
ebbad60e21
bot-backend merged commit f1f2bca2e9 into main 2026-08-09 17:26:19 +00:00
bot-backend deleted branch fix/2659-revisit-floor 2026-08-09 17:26:19 +00:00
Author
Collaborator

Прод-верификация после мержа (read-only)

Код в живых контейнерах — проверено кодом, а не флагом релиза:

tradein-scraper (в нём крутится scheduler) и tradein-backend:
  DEFAULT_REVISIT_FLOOR_QUANTILE: 0.99
  has _build_revisit_floor_sql: True
  revisit_floor_quantile в сигнатуре: True
  handler прокидывает: True

Решение, которое развёрнутый код примет на ближайшем прогоне (симуляция тем же
запросом, что зашит в задачу):

расписание TTL задан пол TTL эффективный снял бы при заданном снимет при эффективном
avito / все сегменты 10 70 70 0 0
cian / vtorichka 30 34 34 0 0
yandex / vtorichka 30 75 75 43 0
domklik / все сегменты 14 — (NULL) 14 0 0

Критерий из описания PR выполнен: 43 активные строки yandex/vtorichka перестают
сниматься
, это ровно те строки, которых TTL сегодня касается на всём проде.

Домклик остаётся под гейтом здоровья независимо от пола: подтверждений за 3 суток
0 при пороге 200 — пол у него NULL (переобходов нет вовсе, сбор стоит), и деактивация
не исполняется по-прежнему из-за #2710, а не из-за этой правки.

## Прод-верификация после мержа (read-only) Код в живых контейнерах — проверено кодом, а не флагом релиза: ``` tradein-scraper (в нём крутится scheduler) и tradein-backend: DEFAULT_REVISIT_FLOOR_QUANTILE: 0.99 has _build_revisit_floor_sql: True revisit_floor_quantile в сигнатуре: True handler прокидывает: True ``` Решение, которое развёрнутый код примет на ближайшем прогоне (симуляция тем же запросом, что зашит в задачу): | расписание | TTL задан | пол | TTL эффективный | снял бы при заданном | снимет при эффективном | |---|---|---|---|---|---| | avito / все сегменты | 10 | 70 | **70** | 0 | 0 | | cian / vtorichka | 30 | 34 | **34** | 0 | 0 | | yandex / vtorichka | 30 | 75 | **75** | **43** | **0** | | domklik / все сегменты | 14 | — (NULL) | 14 | 0 | 0 | Критерий из описания PR выполнен: **43 активные строки yandex/vtorichka перестают сниматься**, это ровно те строки, которых TTL сегодня касается на всём проде. Домклик остаётся под гейтом здоровья независимо от пола: подтверждений за 3 суток 0 при пороге 200 — пол у него NULL (переобходов нет вовсе, сбор стоит), и деактивация не исполняется по-прежнему из-за #2710, а не из-за этой правки.
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#2797
No description provided.