Каталог DOM.РФ в beat: вместо обещания «вернуть после cooldown» — факт о блокировке StormWall и тест, который не даст включить молча #3566

Merged
bot-backend merged 3 commits from fix/ptica-domrf-waf-probe into main 2026-09-17 09:15:51 +00:00
5 changed files with 55 additions and 46 deletions
Showing only changes of commit 7cd15aab71 - Show all commits

View file

@ -1910,17 +1910,19 @@ def trigger_ekburg_permits(
return {"task_id": result.id, "scope": scope, "queued_at": "now"} return {"task_id": result.id, "scope": scope, "queued_at": "now"}
# WAF cooldown guard message (#2443 — DOM.РФ hard-banned this VPS's IP 2026-05-24 # Текст отказа guard'а блокировки DOM.РФ (#2443, #2445 D1). Зонды 20.08, 27.08 и
# после серии failed catalog SSR extras-сессий). Beat schedule для catalog-object # 01.09: наш.дом.рф за StormWall отдаёт серверу «Доступ заблокирован [403]», таймера
# и catalog-flat scrape'ов ОТКЛЮЧЕН по этой же причине (см. beat_schedule.py) — # у этой блокировки нет. Beat-записи catalog-object и catalog-flat scrape'ов
# оба ad-hoc admin-эндпоинта ниже бьют по ТОМУ ЖЕ /сервисы/* BrowserSession # выключены по той же причине (см. beat_schedule.py); оба ad-hoc эндпоинта ниже
# path family, поэтому без явного оператор-override могут углубить бан (#2445 D1). # ходят тем же /сервисы/* BrowserSession path family. Оператор решает по этому
_WAF_COOLDOWN_GUARD_MSG = ( # тексту, ставить ли флаг, поэтому он называет реальное условие (#3307).
"Ad-hoc catalog-scrape заблокирован guard'ом: DOM.РФ WAF hard-ban этого VPS IP " _DOMRF_BLOCK_GUARD_MSG = (
"2026-05-24 (issue #2443), beat schedule для этого таска отключён по той же " "Ad-hoc catalog-scrape заблокирован guard'ом: наш.дом.рф за StormWall отдаёт "
"причине. Повторный ad-hoc запуск может углубить бан. Если ты осознанно " "этому серверу «Доступ заблокирован [403]» (зонды 20.08-01.09, issue #2443), "
"принимаешь этот риск (WAF cooldown прошёл, targeted smoke-test и т.п.) — " "beat schedule для этого таска выключен по той же причине. Ожидание блокировку "
"передай i_understand_waf_risk=true в теле запроса." "не снимает, запуск отсюда даст только отказы. Передавай "
"i_understand_waf_risk=true, только когда сбор идёт через прокси и kn-прогон "
"принят по числу строк (#3307)."
) )
@ -1952,20 +1954,20 @@ def trigger_kn_catalog_objects(
"""Manual trigger для catalog-OBJECT scraper (заполняет wall_type, energy_eff, """Manual trigger для catalog-OBJECT scraper (заполняет wall_type, energy_eff,
ceiling_height_m, parking_*, playground_*, scores из SSR __NEXT_DATA__). ceiling_height_m, parking_*, playground_*, scores из SSR __NEXT_DATA__).
Beat schedule: Tuesday 04:00 UTC, batch 300/run. Этот endpoint для ad-hoc Beat-запись выключена (блокировка StormWall, #2443; см. beat_schedule.py).
запуска (smoke-тест после деплоя или повторный pass для свежесозданных Этот endpoint для ad-hoc запуска (smoke-тест или повторный pass для
объектов до next beat fire). свежесозданных объектов).
- max_objects=None дефолтный лимит таска (300). - max_objects=None дефолтный лимит таска (300).
- max_objects=3 smoke-тест. - max_objects=3 smoke-тест.
- force=True "Загрузить все": игнорирует skip-today, грузит всё подряд. - force=True "Загрузить все": игнорирует skip-today, грузит всё подряд.
WAF cooldown guard (#2443, #2445 D1): требует i_understand_waf_risk=true — Guard блокировки DOM.РФ (#2443, #2445 D1): требует i_understand_waf_risk=true —
beat schedule для этого таска отключён из-за WAF hard-ban 2026-05-24, ad-hoc beat schedule для этого таска выключен из-за блокировки StormWall, ad-hoc
re-trigger без явного подтверждения оператора запрещён. запуск без явного подтверждения оператора запрещён (условие #3307).
""" """
if not payload.i_understand_waf_risk: if not payload.i_understand_waf_risk:
raise HTTPException(status_code=400, detail=_WAF_COOLDOWN_GUARD_MSG) raise HTTPException(status_code=400, detail=_DOMRF_BLOCK_GUARD_MSG)
from app.workers.tasks.scrape_kn_catalog_objects import scrape_kn_catalog_objects from app.workers.tasks.scrape_kn_catalog_objects import scrape_kn_catalog_objects
@ -2022,12 +2024,12 @@ def trigger_kn_catalog_flats(
- max_flats=3 smoke-тест. - max_flats=3 smoke-тест.
- force=True 'Загрузить все': игнорирует фильтр свежести, грузит всё с hash. - force=True 'Загрузить все': игнорирует фильтр свежести, грузит всё с hash.
WAF cooldown guard (#2443, #2445 D1): требует i_understand_waf_risk=true — Guard блокировки DOM.РФ (#2443, #2445 D1): требует i_understand_waf_risk=true —
same /сервисы/* BrowserSession path family как catalog-objects, риск re-trigger same /сервисы/* BrowserSession path family как catalog-objects, закрыт той же
того же WAF-бана. блокировкой StormWall (условие #3307).
""" """
if not payload.i_understand_waf_risk: if not payload.i_understand_waf_risk:
raise HTTPException(status_code=400, detail=_WAF_COOLDOWN_GUARD_MSG) raise HTTPException(status_code=400, detail=_DOMRF_BLOCK_GUARD_MSG)
from app.workers.tasks.scrape_kn_catalog_flats import scrape_kn_catalog_flats from app.workers.tasks.scrape_kn_catalog_flats import scrape_kn_catalog_flats

View file

@ -24,8 +24,9 @@ Sequencing dependency (важно): эта задача НЕ дублирует
1. #2442 Task 1 (elemId → catalog_url_hash) задеплоен, И 1. #2442 Task 1 (elemId → catalog_url_hash) задеплоен, И
2. прошёл свежий kn-sweep, наполнивший catalog_url_hash на новых snapshot'ах. 2. прошёл свежий kn-sweep, наполнивший catalog_url_hash на новых snapshot'ах.
До этого SELECT вернёт 0 строк (см. выше). Beat-entry намеренно оставлен До этого SELECT вернёт 0 строк (см. выше). Beat-entry намеренно оставлен
ЗАКОММЕНТИРОВАННЫМ в beat_schedule.py (как и object-level) WAF-cooldown на ЗАКОММЕНТИРОВАННЫМ в beat_schedule.py (как и object-level): наш.дом.рф за
VPS IP; включается вручную после проверки targeted-тестом. StormWall отдаёт «Доступ заблокирован [403]» (#2443), ожидание это не снимает;
включать после прокси и принятого kn-прогона (#3307).
""" """
from __future__ import annotations from __future__ import annotations
@ -116,7 +117,7 @@ def scrape_kn_catalog_flats(
Concurrency: Concurrency:
No Redis lock consistent с sibling tasks (scrape_kn_catalog_objects и т.д.). No Redis lock consistent с sibling tasks (scrape_kn_catalog_objects и т.д.).
Beat отключён (WAF cooldown); при ручном/ad-hoc запуске пересечение крайне Beat отключён (блокировка StormWall, #2443); при ручном/ad-hoc запуске пересечение крайне
маловероятно. Если случится: маловероятно. Если случится:
- UPDATE идемпотентен (COALESCE в upsert_catalog_data, WHERE ods_id). - UPDATE идемпотентен (COALESCE в upsert_catalog_data, WHERE ods_id).
- Max risk: 2x WAF-нагрузка на DOM.РФ для того же batch. - Max risk: 2x WAF-нагрузка на DOM.РФ для того же batch.

View file

@ -9,7 +9,7 @@ Selector logic:
- DATE(catalog_scraped_at) = CURRENT_DATE уже сегодня обновлён, пропускаем - DATE(catalog_scraped_at) = CURRENT_DATE уже сегодня обновлён, пропускаем
- force=True игнорирует фильтр, загружает все объекты последнего snapshot - force=True игнорирует фильтр, загружает все объекты последнего snapshot
Beat schedule: вторник 04:00 UTC (в beat_schedule.py). Beat-запись (вторник 04:00 МСК) выключена в beat_schedule.py: блокировка StormWall (#2443).
""" """
from __future__ import annotations from __future__ import annotations

View file

@ -1,10 +1,11 @@
"""Тесты для WAF cooldown guard (#2443, #2445 D1) на ad-hoc catalog-scrape эндпоинтах. """Тесты guard'а блокировки DOM.РФ (#2443, #2445 D1) на ad-hoc catalog-scrape эндпоинтах.
POST /api/v1/admin/scrape/kn-catalog-objects и /kn-catalog-flats бьют по тому же POST /api/v1/admin/scrape/kn-catalog-objects и /kn-catalog-flats бьют по тому же
/сервисы/* BrowserSession path family, что вызвал DOM.РФ WAF hard-ban 2026-05-24 /сервисы/* BrowserSession path family, что закрыт StormWall (#2443). Beat schedule
(#2443). Beat schedule для обоих тасков отключён по этой причине — эти эндпоинты для обоих тасков отключён по этой причине эти эндпоинты должны отказывать в
должны отказывать в запуске БЕЗ явного i_understand_waf_risk=true, чтобы случайный запуске БЕЗ явного i_understand_waf_risk=true. Текст отказа то, по чему оператор
smoke-тест/re-trigger не углубил бан. решает, ставить ли флаг, поэтому он обязан называть реальное условие (#3307), а не
ожидание таймера: зонды 20.08-01.09 показали, что ждать нечего.
""" """
from __future__ import annotations from __future__ import annotations
@ -19,6 +20,15 @@ OBJECTS_ENDPOINT = "/api/v1/admin/scrape/kn-catalog-objects"
FLATS_ENDPOINT = "/api/v1/admin/scrape/kn-catalog-flats" FLATS_ENDPOINT = "/api/v1/admin/scrape/kn-catalog-flats"
def _assert_refusal_names_real_condition(detail: str) -> None:
assert "2443" in detail
assert "i_understand_waf_risk" in detail
# Условие снятия — прокси и принятый kn-прогон (#3307). Обещание «cooldown
# прошёл» подсказывало оператору обойти блокировку, которую ожидание не снимает.
assert "3307" in detail
assert "cooldown" not in detail.lower()
def _mock_apply_async_result(task_id: str = "fake-task-id") -> MagicMock: def _mock_apply_async_result(task_id: str = "fake-task-id") -> MagicMock:
result = MagicMock() result = MagicMock()
result.id = task_id result.id = task_id
@ -33,9 +43,7 @@ def test_kn_catalog_objects_refuses_without_override() -> None:
resp = TestClient(app).post(OBJECTS_ENDPOINT, json={"region_code": 66}) resp = TestClient(app).post(OBJECTS_ENDPOINT, json={"region_code": 66})
assert resp.status_code == 400 assert resp.status_code == 400
detail = resp.json()["detail"] _assert_refusal_names_real_condition(resp.json()["detail"])
assert "2443" in detail
assert "i_understand_waf_risk" in detail
mock_apply_async.assert_not_called() mock_apply_async.assert_not_called()
@ -68,9 +76,7 @@ def test_kn_catalog_flats_refuses_without_override() -> None:
resp = TestClient(app).post(FLATS_ENDPOINT, json={"region_code": 66}) resp = TestClient(app).post(FLATS_ENDPOINT, json={"region_code": 66})
assert resp.status_code == 400 assert resp.status_code == 400
detail = resp.json()["detail"] _assert_refusal_names_real_condition(resp.json()["detail"])
assert "2443" in detail
assert "i_understand_waf_risk" in detail
mock_apply_async.assert_not_called() mock_apply_async.assert_not_called()

View file

@ -1919,17 +1919,17 @@ export interface paths {
* @description Manual trigger для catalog-OBJECT scraper (заполняет wall_type, energy_eff, * @description Manual trigger для catalog-OBJECT scraper (заполняет wall_type, energy_eff,
* ceiling_height_m, parking_*, playground_*, scores из SSR __NEXT_DATA__). * ceiling_height_m, parking_*, playground_*, scores из SSR __NEXT_DATA__).
* *
* Beat schedule: Tuesday 04:00 UTC, batch 300/run. Этот endpoint для ad-hoc * Beat-запись выключена (блокировка StormWall, #2443; см. beat_schedule.py).
* запуска (smoke-тест после деплоя или повторный pass для свежесозданных * Этот endpoint для ad-hoc запуска (smoke-тест или повторный pass для
* объектов до next beat fire). * свежесозданных объектов).
* *
* - max_objects=None дефолтный лимит таска (300). * - max_objects=None дефолтный лимит таска (300).
* - max_objects=3 smoke-тест. * - max_objects=3 smoke-тест.
* - force=True "Загрузить все": игнорирует skip-today, грузит всё подряд. * - force=True "Загрузить все": игнорирует skip-today, грузит всё подряд.
* *
* WAF cooldown guard (#2443, #2445 D1): требует i_understand_waf_risk=true * Guard блокировки DOM.РФ (#2443, #2445 D1): требует i_understand_waf_risk=true
* beat schedule для этого таска отключён из-за WAF hard-ban 2026-05-24, ad-hoc * beat schedule для этого таска выключен из-за блокировки StormWall, ad-hoc
* re-trigger без явного подтверждения оператора запрещён. * запуск без явного подтверждения оператора запрещён (условие #3307).
*/ */
post: operations["trigger_kn_catalog_objects_api_v1_admin_scrape_kn_catalog_objects_post"]; post: operations["trigger_kn_catalog_objects_api_v1_admin_scrape_kn_catalog_objects_post"];
delete?: never; delete?: never;
@ -1960,9 +1960,9 @@ export interface paths {
* - max_flats=3 smoke-тест. * - max_flats=3 smoke-тест.
* - force=True 'Загрузить все': игнорирует фильтр свежести, грузит всё с hash. * - force=True 'Загрузить все': игнорирует фильтр свежести, грузит всё с hash.
* *
* WAF cooldown guard (#2443, #2445 D1): требует i_understand_waf_risk=true * Guard блокировки DOM.РФ (#2443, #2445 D1): требует i_understand_waf_risk=true
* same /сервисы/* BrowserSession path family как catalog-objects, риск re-trigger * same /сервисы/* BrowserSession path family как catalog-objects, закрыт той же
* того же WAF-бана. * блокировкой StormWall (условие #3307).
*/ */
post: operations["trigger_kn_catalog_flats_api_v1_admin_scrape_kn_catalog_flats_post"]; post: operations["trigger_kn_catalog_flats_api_v1_admin_scrape_kn_catalog_flats_post"];
delete?: never; delete?: never;