vector-auditor / src /bootstrap.py
AkshayM
fix: 15 critical/high bugs across codebase
da3be01
Raw
History Blame Contribute Delete
5.03 kB
"""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)
@asynccontextmanager
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