-- 219_deactivate_stale_health_gate.sql -- Пороги гейта здоровья сбора для TTL-деактивации (#2659). -- -- ЗАЧЕМ. TTL отвечает на вопрос «объявление сняли?», а меряет «мы его давно не -- видели». Пока обход здоров, разница мала; когда обход лёг — разница равна всему -- инвентарю. Прод, авито 10.07-26.07.2026: 17 суток подряд без единой собранной -- страницы, TTL=10 снял 9 033 строки, из них 1 270 доказанно вернулись живыми, -- как только сбор восстановился (сверка listing_source_snapshots с текущим -- last_seen_at). Код гейта — app/tasks/deactivate_stale_avito.py. -- -- ПОЧЕМУ НЕ ПО СТАТУСУ ПРОГОНА. Ban-детектор эти провалы НЕ ловит: -- yandex 18.07-30.07 — 5 прогонов в сутки, ВСЕ 'done', НОЛЬ 'banned', -- total_seen = 0 тринадцать суток подряд; -- domklik 20.07-30.07 — 11 суток 'done' с total_seen = 0, а 02.08 TTL=14 -- снял 6 131 строку разом. -- Поэтому здоровье меряется результатом (сколько строк источник реально подтвердил -- свежими за 3 суток), а не статусом прогона. -- -- ОТКУДА ЧИСЛА. Ряд «подтверждений за 3 суток» по дням восстановлен из -- listing_source_snapshots (снимок last_seen_at на каждую дату), срез совпадает -- со срезом соответствующего UPDATE (source + segments + та же колонка свежести): -- avito (все сегменты, last_seen_at): здоровые сутки 3542..6079, -- провал 08.07-29.07 — 0..970 -> 1500 -- yandex (vtorichka, last_seen_at): здоровые 897..2206, провал 0 -> 500 -- cian (vtorichka, last_seen_at): 748..4329, провалов не было -> 500 -- domklik (все сегменты, scraped_at): сейчас 62 за 3 суток, сбор -- фактически стоит -> 200 -- Каждый порог лежит между максимумом провала и минимумом здоровых суток: -- авито 970 < 1500 < 3542 — исторический случай ловится с запасом в обе стороны. -- -- domklik ЗАБЛОКИРУЕТСЯ СРАЗУ, и это верный исход, а не сбой миграции: источник -- подтверждает ~50 строк в сутки, TTL по нему уже один раз (02.08) снёс инвентарь -- целиком. Пока сбор не восстановлен, деактивации там нечего подтверждать; протухшие -- строки закрываются фильтром свежести на стороне чтения (#2656), а не TTL. -- -- Цена ошибки асимметрична: пропущенная деактивация чинится следующим прогоном, -- ложная — только повторным сбором, которого может не быть. Пороги поэтому -- смещены в сторону «пропустить прогон». -- -- ЗАВИСИМОСТИ: 052_scrape_schedules.sql (таблица + UNIQUE(source)), -- 090/115/160 (сами расписания deactivate_stale_*). -- ТОЛЬКО данные (UPDATE default_params), DDL нет. -- Идемпотентность + уважение к ручной настройке: ключ проставляется лишь там, где -- его ещё нет, поэтому повторный прогон файла не затирает подкрученное оператором -- значение. Снять гейт вручную: min_confirmations = 0. BEGIN; UPDATE scrape_schedules SET default_params = default_params || jsonb_build_object('min_confirmations', 1500), updated_at = NOW() WHERE source = 'deactivate_stale_avito' AND NOT default_params ? 'min_confirmations'; UPDATE scrape_schedules SET default_params = default_params || jsonb_build_object('min_confirmations', 500), updated_at = NOW() WHERE source IN ('deactivate_stale_cian', 'deactivate_stale_yandex') AND NOT default_params ? 'min_confirmations'; UPDATE scrape_schedules SET default_params = default_params || jsonb_build_object('min_confirmations', 200), updated_at = NOW() WHERE source = 'deactivate_stale_domklik' AND NOT default_params ? 'min_confirmations'; COMMIT;