Avinash Nalla
Phase 5: split scheduler into standalone process; enable uvicorn workers
b96346f
Raw
History Blame Contribute Delete
1.94 kB
import logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from app.config import settings
from app.database import init_db
from app.lib.graph.auth import GraphAuthProvider
from app.lib.graph.client import GraphClient
from app.routes.health import router as health_router
from app.routes.webhook import router as webhook_router
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator:
"""Minimal lifespan — sets up the DB and Graph client only.
Subscription bootstrap and APScheduler jobs live in the standalone
`app.scheduler_main` process so that the FastAPI server can run with
multiple uvicorn workers without duplicating cron work or racing on
subscription creation. The webhook handler reads the active
subscription's clientState from the DB on each request — it does not
need a live subscription_id on app.state.
"""
# --- Startup ---
await init_db(settings.database_path)
auth = GraphAuthProvider(settings)
client = GraphClient(auth)
app.state.client = client
app.state.settings = settings
logger.info("FastAPI ready — webhook receiver online")
yield
# --- Shutdown ---
await client.aclose()
logger.info("Shutdown complete")
app = FastAPI(title="RCMEmail", version="1.0.0", lifespan=lifespan)
app.include_router(health_router)
app.include_router(webhook_router)
@app.exception_handler(Exception)
async def _global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
logger.exception("Unhandled exception for %s %s", request.method, request.url.path)
return JSONResponse(status_code=500, content={"detail": "Internal server error"})