Spaces:
Running
Running
| """Bootstrap — dependency-ordered init at app startup. | |
| Loads embedding model, pre-warms LLM, starts job-queue worker, installs signal handlers. | |
| Fail-fast in production, warn-and-continue in development. | |
| """ | |
| import asyncio | |
| import logging | |
| import os | |
| import time | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI | |
| from .config import get_settings | |
| from .database.session import dispose_engine, init_engine | |
| from .job_queue import get_worker | |
| from .observability import get_metrics, setup_observability | |
| from .services.cache import get_cache | |
| from .services.cloudinary import get_cloudinary | |
| from .services.guardrails import get_guardrails | |
| from .services.llm import get_llm | |
| from .services.pii_detector import get_pii_detector | |
| from .shutdown import get_shutdown_manager | |
| from .vectorstore.Qdrant import get_vector_store | |
| logger = logging.getLogger("rga_auditor.bootstrap") | |
| StartupError = RuntimeError | |
| async def bootstrap_app() -> dict: | |
| """Initialize all subsystems in dependency order. Returns a status dict.""" | |
| s = get_settings() | |
| setup_observability() | |
| t0 = time.time() | |
| status: dict = {"steps": []} | |
| def step(name: str, fn): | |
| t = time.time() | |
| try: | |
| result = fn() | |
| dt = time.time() - t | |
| status["steps"].append({"name": name, "ok": True, "ms": int(dt * 1000)}) | |
| logger.info("bootstrap[%s] OK in %dms", name, int(dt * 1000)) | |
| return result | |
| except Exception as e: | |
| dt = time.time() - t | |
| status["steps"].append({"name": name, "ok": False, "ms": int(dt * 1000), "error": str(e)}) | |
| if s.is_production: | |
| logger.critical("bootstrap[%s] FAILED: %s", name, e) | |
| raise StartupError(f"bootstrap step '{name}' failed: {e}") from e | |
| logger.warning("bootstrap[%s] failed (dev mode, continuing): %s", name, e) | |
| return None | |
| t_db = time.time() | |
| try: | |
| await init_engine() | |
| status["steps"].append({"name": "database", "ok": True, "ms": int((time.time() - t_db) * 1000)}) | |
| logger.info("bootstrap[database] OK in %dms", int((time.time() - t_db) * 1000)) | |
| except Exception as e: | |
| status["steps"].append({"name": "database", "ok": False, "ms": int((time.time() - t_db) * 1000), "error": str(e)}) | |
| if s.is_production: | |
| logger.critical("bootstrap[database] FAILED: %s", e) | |
| raise StartupError(f"database init failed: {e}") from e | |
| logger.warning("bootstrap[database] failed (dev mode, continuing): %s", e) | |
| # 2. cache | |
| step("cache", get_cache) | |
| step("vector_store", get_vector_store) | |
| # 4. guardrails + pii (best-effort) | |
| step("guardrails", get_guardrails) | |
| step("pii_detector", get_pii_detector) | |
| step("cloudinary", get_cloudinary) | |
| # 5. LLM pre-warm (optional) | |
| if s.skip_llm_prewarm: | |
| logger.info("bootstrap[llm] SKIPPED (SKIP_LLM_PREWARM=true)") | |
| else: | |
| def _prewarm(): | |
| llm = get_llm() | |
| try: | |
| # Synchronous prewarm: we can't await here; the LLM client's chat is async | |
| return llm | |
| except Exception as e: | |
| if s.is_production: | |
| raise | |
| logger.warning("LLM prewarm failed: %s", e) | |
| step("llm", _prewarm) | |
| # actually prewarm async | |
| try: | |
| await get_llm().chat("ping", system="You are a test. Reply with 'pong'.", max_tokens=10) | |
| logger.info("LLM prewarm OK") | |
| except Exception as e: | |
| if s.is_production: | |
| raise StartupError(f"LLM prewarm failed: {e}") | |
| logger.warning("LLM prewarm failed: %s", e) | |
| # 6. job queue worker | |
| await get_worker().start() | |
| # 7. signal handlers | |
| try: | |
| loop = asyncio.get_running_loop() | |
| get_shutdown_manager().install_signal_handlers(loop) | |
| except Exception as e: | |
| logger.debug("signal handlers skipped: %s", e) | |
| # 8. metrics timestamp | |
| get_metrics().startup_timestamp.set(time.time()) | |
| status["ready_at"] = time.time() | |
| status["ready_in_ms"] = int((time.time() - t0) * 1000) | |
| logger.info("Application READY in %dms", status["ready_in_ms"]) | |
| return status | |
| async def shutdown_app() -> None: | |
| """Tear down in reverse order.""" | |
| logger.info("Application shutting down") | |
| try: | |
| mgr = get_shutdown_manager() | |
| mgr.begin_shutdown() | |
| await mgr.wait_drain() | |
| except Exception as e: | |
| logger.warning("drain: %s", e) | |
| try: | |
| await get_worker().stop() | |
| except Exception as e: | |
| logger.warning("worker stop: %s", e) | |
| try: | |
| await dispose_engine() | |
| except Exception as e: | |
| logger.warning("db dispose: %s", e) | |
| async def lifespan(app: FastAPI): | |
| await bootstrap_app() | |
| yield | |
| await shutdown_app() | |
| def install_bootstrap(app: FastAPI) -> None: | |
| """Attach the bootstrap lifespan to an existing FastAPI app.""" | |
| app.router.lifespan_context = lifespan | |