diff --git a/backend/app/workers/beat_schedule.py b/backend/app/workers/beat_schedule.py index e7ce5146..fb7a197b 100644 --- a/backend/app/workers/beat_schedule.py +++ b/backend/app/workers/beat_schedule.py @@ -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()] -def _build_beat_schedule_from_db() -> dict: +def _build_beat_schedule_from_db() -> tuple[dict, int]: """Строит beat_schedule из job_settings БД. - Возвращает пустой словарь при любой ошибке — caller добавит fallback. - """ - try: - from app.core.db import SessionLocal - from app.services.job_settings import get_all_safe + Returns: (schedule, rows_seen). + - rows_seen > 0 → DB reachable, есть конфигурация job_settings (даже если все disabled) + - rows_seen == 0 → DB reachable, но таблица пустая (fresh install до seed'а) + - **RAISES** при недоступности БД (table missing, connection refused и т.п.) — + caller должен поймать и использовать env-based fallback как safety net. - rows = get_all_safe(SessionLocal) - except Exception as e: - logger.warning("build_beat_schedule: не удалось прочитать job_settings: %s", e) - return {} + Используем `get_all` (strict, raises on failure), НЕ `get_all_safe` — нам важно + отличить "DB unreachable" от "DB сказал ничего не schedule'ить". + Без этого пара UPDATE job_settings SET enabled=false бессильна: + `_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 = {} 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) - return schedule + return schedule, len(rows) def _build_beat_schedule_fallback() -> dict: @@ -168,18 +179,27 @@ def _build_beat_schedule_fallback() -> 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 = {} + rows_seen = 0 + db_reachable = True try: - schedule = _build_beat_schedule_from_db() + schedule, rows_seen = _build_beat_schedule_from_db() 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: - # БД пустая или недоступна — полный fallback + if not db_reachable or rows_seen == 0: + # DB недоступна ИЛИ fresh install (job_settings пустая) → env-based safety net return _build_beat_schedule_fallback() # Всегда добавляем refresh_analytics (этот job нет в job_settings,