feat(P01): TASK-01-03 asyncpg pool lifespan + graceful degradation
Add @asynccontextmanager lifespan to the FastAPI app that creates an asyncpg pool (min=1, max=10, command_timeout=10 — D-050) on app.state.pg_pool and a PgStore on app.state.pg_store when PRAXIS_PG_DSN is set, applies pg_migrations on startup, and closes the pool on shutdown. Graceful degradation (REQ-NFR-MT-01): if PRAXIS_PG_DSN is unset, the server starts with a WARNING and pg_pool/pg_store are None. The learner voice loop (SQLite PraxisStore) is unaffected. Auth/operator routes will return 503 (wired in SLICE-06). ---ci--- project: praxis phase: 1 milestone: v0.4 status: execute persona: backend-engineer task: 01-03 requirements: covered: [REQ-MT-01, REQ-NFR-MT-01] ---/ci---
This commit is contained in:
+52
-1
@@ -14,6 +14,7 @@ no audio/no tokens at runtime, not a crash.
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
@@ -32,6 +33,8 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection
|
||||
|
||||
from db.pg_migrate import apply_pg_migrations
|
||||
from db.pg_store import PgStore
|
||||
from db.store import PraxisStore
|
||||
from server.pipeline import build_pipeline
|
||||
from server.vc.verification import verify_credential
|
||||
@@ -47,6 +50,54 @@ HOST = _env("PRAXIS_HOST", "0.0.0.0")
|
||||
PORT = int(_env("PRAXIS_PORT", "8789"))
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""v0.4 — create the asyncpg Postgres pool on startup, close on shutdown.
|
||||
|
||||
Graceful degradation (D-050, REQ-NFR-MT-01): if PRAXIS_PG_DSN is unset,
|
||||
the server starts without Postgres — the learner voice loop (SQLite)
|
||||
is unaffected. app.state.pg_pool / app.state.pg_store are None in that
|
||||
case and auth/operator routes return 503.
|
||||
"""
|
||||
dsn = os.environ.get("PRAXIS_PG_DSN", "").strip()
|
||||
if not dsn:
|
||||
logger.warning(
|
||||
"PRAXIS_PG_DSN not set — starting without Postgres (dev/no-pool mode). "
|
||||
"Operator auth + cohort endpoints will be unavailable (503). "
|
||||
"Learner voice loop (SQLite) is unaffected."
|
||||
)
|
||||
app.state.pg_pool = None
|
||||
app.state.pg_store = None
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
return
|
||||
import asyncpg
|
||||
|
||||
logger.info("Creating asyncpg Postgres pool (min=1, max=10, D-050)")
|
||||
pool = await asyncpg.create_pool(
|
||||
dsn=dsn,
|
||||
min_size=1,
|
||||
max_size=10,
|
||||
command_timeout=10,
|
||||
)
|
||||
app.state.pg_pool = pool
|
||||
app.state.pg_store = PgStore(pool)
|
||||
try:
|
||||
applied = await apply_pg_migrations(pool)
|
||||
if applied:
|
||||
logger.info(f"Postgres migrations applied: {applied}")
|
||||
else:
|
||||
logger.info("Postgres migrations up to date")
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
pass
|
||||
finally:
|
||||
await pool.close()
|
||||
logger.info("Postgres pool closed")
|
||||
|
||||
|
||||
class WebRTCOffer(BaseModel):
|
||||
"""Client→server WebRTC offer (SDP + type)."""
|
||||
|
||||
@@ -54,7 +105,7 @@ class WebRTCOffer(BaseModel):
|
||||
type: str = "offer"
|
||||
|
||||
|
||||
app = FastAPI(title="Praxis v0.1 voice server", version="0.1.0")
|
||||
app = FastAPI(title="Praxis v0.1 voice server", version="0.1.0", lifespan=lifespan)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # dev — the client is a separate Vite origin
|
||||
|
||||
Reference in New Issue
Block a user