fix(beat): distinguish DB-unreachable from intentional-empty schedule
UPDATE job_settings SET enabled=false для всех cron-able jobs приводил к
пустому schedule из DB — caller (build_beat_schedule) интерпретировал
'not schedule' как 'DB error' и срывался на env-based fallback, который
повторно добавлял scrape_kn + objective_sync из settings.scrape_kn_cron /
objective_sync_cron. Disable через DB был **бесполезен**.
Incident 2026-05-24: WAF hard-ban на VPS IP от DOM.РФ → попытка отключить
scrape_kn + objective_sync через UPDATE job_settings — fallback вернул их
обратно. Беспорядок виден в beat logs: 'beat_schedule: строим fallback из
env vars' при наличии rows в БД.
Fix:
- `_build_beat_schedule_from_db` теперь использует `get_all` (strict, raises)
вместо `get_all_safe` (silently возвращает _DEFAULTS)
- Возвращает (schedule, rows_seen) tuple
- Caller fallback'ает ТОЛЬКО при `not db_reachable` (exception) ИЛИ
`rows_seen == 0` (fresh install, безопасный fallback для setup)
- При rows_seen > 0 + schedule={} — RESPECT user intent (все disabled)
- Hardcoded entries (OSM POI/noise, nspd cleanup, refresh_analytics)
добавляются ВСЕГДА сверху — они не управляются job_settings
This commit is contained in:
parent
ae0fce3528
commit
dc1cd943e6
1 changed files with 37 additions and 17 deletions
|
|
@ -50,19 +50,30 @@ def _default_regions() -> list[int]:
|
||||||
return [int(x.strip()) for x in settings.scrape_kn_default_regions.split(",") if x.strip()]
|
return [int(x.strip()) for x in settings.scrape_kn_default_regions.split(",") if x.strip()]
|
||||||
|
|
||||||
|
|
||||||
def _build_beat_schedule_from_db() -> dict:
|
def _build_beat_schedule_from_db() -> tuple[dict, int]:
|
||||||
"""Строит beat_schedule из job_settings БД.
|
"""Строит beat_schedule из job_settings БД.
|
||||||
|
|
||||||
Возвращает пустой словарь при любой ошибке — caller добавит fallback.
|
Returns: (schedule, rows_seen).
|
||||||
"""
|
- rows_seen > 0 → DB reachable, есть конфигурация job_settings (даже если все disabled)
|
||||||
try:
|
- rows_seen == 0 → DB reachable, но таблица пустая (fresh install до seed'а)
|
||||||
from app.core.db import SessionLocal
|
- **RAISES** при недоступности БД (table missing, connection refused и т.п.) —
|
||||||
from app.services.job_settings import get_all_safe
|
caller должен поймать и использовать env-based fallback как safety net.
|
||||||
|
|
||||||
rows = get_all_safe(SessionLocal)
|
Используем `get_all` (strict, raises on failure), НЕ `get_all_safe` — нам важно
|
||||||
except Exception as e:
|
отличить "DB unreachable" от "DB сказал ничего не schedule'ить".
|
||||||
logger.warning("build_beat_schedule: не удалось прочитать job_settings: %s", e)
|
Без этого пара UPDATE job_settings SET enabled=false бессильна:
|
||||||
return {}
|
`_build_beat_schedule_from_db` вернул бы {} как при DB error → caller fallback'ает
|
||||||
|
на env-cron → отключённые jobs снова в schedule. См. инцидент 2026-05-24
|
||||||
|
(WAF cooldown disable scrape_kn → fallback вернул его обратно).
|
||||||
|
"""
|
||||||
|
from app.core.db import SessionLocal
|
||||||
|
from app.services.job_settings import get_all
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
rows = get_all(db)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
schedule: dict = {}
|
schedule: dict = {}
|
||||||
for row in rows:
|
for row in rows:
|
||||||
|
|
@ -117,7 +128,7 @@ def _build_beat_schedule_from_db() -> dict:
|
||||||
}
|
}
|
||||||
logger.info("build_beat_schedule: %s cron=%r queue=%s", jt, cron_str, queue)
|
logger.info("build_beat_schedule: %s cron=%r queue=%s", jt, cron_str, queue)
|
||||||
|
|
||||||
return schedule
|
return schedule, len(rows)
|
||||||
|
|
||||||
|
|
||||||
def _build_beat_schedule_fallback() -> dict:
|
def _build_beat_schedule_fallback() -> dict:
|
||||||
|
|
@ -168,18 +179,27 @@ def _build_beat_schedule_fallback() -> dict:
|
||||||
|
|
||||||
|
|
||||||
def build_beat_schedule() -> dict:
|
def build_beat_schedule() -> dict:
|
||||||
"""Строит beat_schedule: сначала из DB, при неудаче — fallback на env.
|
"""Строит beat_schedule:
|
||||||
|
- Из DB (job_settings) если DB reachable И rows > 0
|
||||||
|
- Env-based fallback ТОЛЬКО когда DB unreachable ИЛИ table пустая (fresh install)
|
||||||
|
- Пустой schedule с rows>0 (все jobs disabled намеренно) — RESPECT, без fallback
|
||||||
|
|
||||||
Всегда добавляет refresh-ekb-districts-medians (нет в job_settings).
|
Всегда добавляет hardcoded entries поверх (refresh-analytics, OSM POI/noise,
|
||||||
|
nspd cleanup) — они не управляются через job_settings.
|
||||||
"""
|
"""
|
||||||
schedule: dict = {}
|
schedule: dict = {}
|
||||||
|
rows_seen = 0
|
||||||
|
db_reachable = True
|
||||||
try:
|
try:
|
||||||
schedule = _build_beat_schedule_from_db()
|
schedule, rows_seen = _build_beat_schedule_from_db()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("build_beat_schedule: неожиданная ошибка: %s — fallback", e)
|
logger.error(
|
||||||
|
"build_beat_schedule: DB unreachable: %s — fallback на env vars (safety net)", e
|
||||||
|
)
|
||||||
|
db_reachable = False
|
||||||
|
|
||||||
if not schedule:
|
if not db_reachable or rows_seen == 0:
|
||||||
# БД пустая или недоступна — полный fallback
|
# DB недоступна ИЛИ fresh install (job_settings пустая) → env-based safety net
|
||||||
return _build_beat_schedule_fallback()
|
return _build_beat_schedule_fallback()
|
||||||
|
|
||||||
# Всегда добавляем refresh_analytics (этот job нет в job_settings,
|
# Всегда добавляем refresh_analytics (этот job нет в job_settings,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue