"""Фоновая инициализация: миграции Alembic и регистрация API после старта HTTP-сервера.""" from __future__ import annotations import logging import os import threading from typing import Callable, List, Optional from flask import Flask from app.services.startup_state import ( mark_startup_failed, mark_startup_ready, reset_startup_state, set_startup_phase, ) _log = logging.getLogger(__name__) _post_ready_callbacks: List[Callable[[Flask], None]] = [] _startup_thread_started = False _startup_thread_lock = threading.Lock() def register_post_ready_callback(fn: Callable[[Flask], None]) -> None: _post_ready_callbacks.append(fn) def is_reloader_parent_process(app: Flask) -> bool: """Родитель Flask reloader (watchdog): не трогает БД, запросы не обслуживает.""" if not app.debug: return False if os.environ.get("WERKZEUG_RUN_MAIN") is not None: return False try: from werkzeug.serving import is_running_from_reloader if is_running_from_reloader(): return False except ImportError: pass return True def should_run_background_startup(app: Flask) -> bool: """Фоновый поток миграций — один раз на рабочий процесс.""" if app.config.get("TESTING"): return False if os.environ.get("WESP_ALEMBIC", "").strip() == "1": return False if not app.config.get("WESP_BACKGROUND_STARTUP", True): return False if is_reloader_parent_process(app): return False return True def _finish_app_startup(app: Flask) -> None: """После миграций: отложенные фоновые службы (маршруты sklad — в create_app).""" set_startup_phase("services", "Подключение модулей…") if app.config.get("SYNC_BACKGROUND_REQUEUE", True): from app.services.startup_defer import schedule_sync_requeue_after_boot schedule_sync_requeue_after_boot(app) try: import psutil # noqa: F401 psutil.cpu_percent(interval=None) except Exception: _log.debug("psutil: прогрев при старте", exc_info=True) try: from app.services.startup_defer import schedule_mdns_after_boot schedule_mdns_after_boot(app) except Exception: _log.warning("mDNS не запланирован при старте", exc_info=True) from app.services.startup_defer import schedule_bootstrap_update_env schedule_bootstrap_update_env(app) if not app.config.get("TESTING"): try: from app.lab.commands.seed_demo_component_nutrients import seed_demo_component_nutrients_once seed_demo_component_nutrients_once() except Exception: _log.debug("Разовый seed nutrients компонентов", exc_info=True) def run_blocking_startup_tasks(app: Flask) -> None: """Синхронные миграции + догонка схемы + finish (для тестов и режима без фона).""" from app import _run_auto_migrations if not app.config.get("TESTING"): try: from config import repair_sync_client_state_on_disk repair_sync_client_state_on_disk() except Exception: _log.debug("repair_sync_client_state_on_disk", exc_info=True) _run_auto_migrations(app) _finish_app_startup(app) mark_startup_ready() def _background_startup_worker(app: Flask) -> None: reset_startup_state() set_startup_phase("migrations", "Миграции базы данных…") try: with app.app_context(): if not app.config.get("TESTING"): try: from config import repair_sync_client_state_on_disk repair_sync_client_state_on_disk() except Exception: _log.debug("repair_sync_client_state_on_disk", exc_info=True) from app import _run_auto_migrations _run_auto_migrations(app) _finish_app_startup(app) mark_startup_ready("Система готова") for cb in list(_post_ready_callbacks): try: cb(app) except Exception: _log.exception("post-ready callback failed") except Exception as exc: _log.exception("Фоновая инициализация WESP не удалась") mark_startup_failed(exc) def schedule_background_startup(app: Flask) -> bool: """Запускает поток инициализации один раз на процесс. Возвращает True, если поток стартовал.""" global _startup_thread_started if is_reloader_parent_process(app): _log.info( "Старт reloader (родитель): миграции пропущены — инициализация в дочернем процессе" ) return False if not should_run_background_startup(app): if not is_startup_ready_implicit(app): run_blocking_startup_tasks(app) return False with _startup_thread_lock: if _startup_thread_started: return False _startup_thread_started = True reset_startup_state() set_startup_phase("migrations", "Миграции базы данных…") threading.Thread( target=_background_startup_worker, args=(app,), name="wesp-background-startup", daemon=True, ).start() return True def is_startup_ready_implicit(app: Flask) -> bool: from app.services.startup_state import is_startup_ready if is_startup_ready(): return True if not app.config.get("WESP_DEFERRED_STARTUP"): return True return False