@@ -0,0 +1,176 @@
|
||||
"""Фоновая инициализация: миграции 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
|
||||
Reference in New Issue
Block a user