rex/startup.py
Rama master · réplica de lectura del repositorio original.
"""Startup sequence enforcement for Rex Flask applications.
Enforces the required initialization order and logs each step at INFO so that
the log stream shows exactly where a failure occurred:
Step 1/4 — Config validation (environment variables are sane)
Step 2/4 — Database connectivity (database file is accessible)
Step 3/4 — Migration state (no pending migrations)
Step 4/4 — Service initialization (caller registers blueprints, starts server)
If any step raises an exception or calls ``sys.exit``, subsequent steps do not
run. The process exits with code 1 when a step fails.
Usage::
from rex.startup import run_startup_sequence
run_startup_sequence() # must be called before Flask app setup
app = Flask(__name__) # step 4: caller completes service init
# … register blueprints …
logger.info("Startup complete — accepting traffic")
"""
from __future__ import annotations
import logging
import sys
from pathlib import Path
from rex.audio.speaker_discovery import start_smart_speaker_discovery
logger = logging.getLogger(__name__)
_TOTAL_STEPS = 4
def _log_step(n: int, description: str) -> None:
logger.info("Startup [step %d/%d]: %s", n, _TOTAL_STEPS, description)
def run_startup_sequence() -> None:
"""Run the startup sequence in order.
Logs each step at INFO level. Exits with code 1 if any step fails so
that subsequent steps never execute.
Raises:
SystemExit: with code 1 if any startup step fails.
"""
_run_config_validation()
_run_database_connectivity()
_run_migration_check()
start_smart_speaker_discovery()
# ---------------------------------------------------------------------------
# Step 1: Config validation
# ---------------------------------------------------------------------------
def _run_config_validation() -> None:
_log_step(1, "Config validation — verifying environment")
try:
_validate_env()
except SystemExit:
raise
except Exception as exc:
logger.critical("Config validation failed: %s", exc)
sys.exit(1)
def _validate_env() -> None:
"""Check that the runtime environment is minimally sane.
Currently a lightweight check. Add mandatory variable assertions here as
requirements grow. Logs warnings for advisory-only issues.
"""
from rex.credentials import get_persisted_credential # noqa: PLC0415
proxy_token = get_persisted_credential("REX_PROXY_TOKEN")
if not proxy_token:
logger.warning("REX_PROXY_TOKEN is not stored — bearer-token auth is disabled.")
logger.debug("Config validation passed")
# ---------------------------------------------------------------------------
# Step 2: Database connectivity
# ---------------------------------------------------------------------------
def _run_database_connectivity() -> None:
_log_step(2, "Database connectivity — verifying database is accessible")
try:
_check_database_connectivity()
except SystemExit:
raise
except Exception as exc:
logger.critical("Database connectivity check failed: %s", exc)
sys.exit(1)
def _check_database_connectivity() -> None:
"""Probe the SQLite database for basic read/write access.
Uses the same default path as the migrations module so that the check is
consistent with the migration state step.
"""
import sqlite3 # noqa: PLC0415
db_path = Path("data/dashboard_notifications.db")
# Ensure the data directory exists (non-fatal; SQLite will create the file)
try:
db_path.parent.mkdir(parents=True, exist_ok=True)
except OSError as exc:
logger.warning("Could not create data directory: %s", exc)
try:
conn = sqlite3.connect(str(db_path), timeout=5.0)
conn.execute("SELECT 1")
conn.close()
logger.debug("Database connectivity OK: %s", db_path)
except sqlite3.Error as exc:
raise RuntimeError(f"Cannot connect to database {db_path}: {exc}") from exc
# ---------------------------------------------------------------------------
# Step 3: Migration state
# ---------------------------------------------------------------------------
def _run_migration_check() -> None:
_log_step(3, "Migration state — checking for pending database migrations")
try:
from rex.migrations import validate_migration_state # noqa: PLC0415
validate_migration_state()
logger.debug("Migration state check passed")
except SystemExit:
raise
except Exception as exc:
logger.critical("Migration state check failed: %s", exc)
sys.exit(1)
# ---------------------------------------------------------------------------
# Step 4 helper — called by the application entry point
# ---------------------------------------------------------------------------
def log_service_ready() -> None:
"""Log step 4 after Flask setup and blueprint registration are complete."""
_log_step(4, "Service initialization complete — ready to accept traffic")
__all__ = [
"run_startup_sequence",
"log_service_ready",
]