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:
Praxis CI
2026-08-04 00:47:30 +00:00
parent b0cb6280d7
commit fb109337a5
+52 -1
View File
@@ -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