"""Health-Checks für Web und Worker (→ Betrieb). live: der Prozess antwortet (im Worker: die Schleife läuft). Bewusst ohne Datenbank, damit ein DB-Ausfall nicht zu Neustarts aller Prozesse führt. ready: Datenbank erreichbar, Migrationen auf dem neuesten Stand, Upload-Verzeichnis beschreibbar, Master-Key gesetzt. Sonst 503: kein Traffic bzw. keine Runs. """ import logging import tempfile from dataclasses import dataclass, field from functools import cache from pathlib import Path from alembic.script import ScriptDirectory from sqlalchemy import text from app.core.config import Settings from app.core.database import Database log = logging.getLogger(__name__) MIGRATIONS = Path(__file__).resolve().parent.parent / "migrations" @dataclass class Report: checks: dict[str, str] = field(default_factory=dict) # Name → "ok" oder Grund @property def ok(self) -> bool: return all(v == "ok" for v in self.checks.values()) def body(self) -> dict: return {"status": "ok" if self.ok else "unavailable", "checks": self.checks} @cache def head_revision() -> str: from alembic.config import Config config = Config() config.set_main_option("script_location", str(MIGRATIONS)) return ScriptDirectory.from_config(config).get_current_head() def ready(db: Database, settings: Settings) -> Report: report = Report() try: with db.engine.connect() as connection: connection.execute(text("SELECT 1")) current = connection.execute(text("SELECT version_num FROM alembic_version")).scalar() report.checks["database"] = "ok" report.checks["migrations"] = "ok" if current == head_revision() else \ f"Stand {current}, erwartet {head_revision()} (aiw migrate)" except Exception as e: report.checks["database"] = f"nicht erreichbar: {type(e).__name__}" report.checks["uploads"] = writable(Path(settings.upload_dir)) report.checks["secret_key"] = "ok" if settings.secret_key else "AIW_SECRET_KEY fehlt" if not report.ok: log.warning("Nicht bereit", extra={"checks": report.checks}) return report def writable(directory: Path) -> str: try: directory.mkdir(parents=True, exist_ok=True) with tempfile.NamedTemporaryFile(dir=directory, prefix=".ready-", delete=True): pass return "ok" except OSError as e: return f"nicht beschreibbar: {e.strerror or type(e).__name__}"