From fb109337a58f08efe09aacad34ff09ec6f14d91b Mon Sep 17 00:00:00 2001 From: Praxis CI Date: Tue, 4 Aug 2026 00:47:30 +0000 Subject: [PATCH] feat(P01): TASK-01-03 asyncpg pool lifespan + graceful degradation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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--- --- server/__main__.py | 53 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/server/__main__.py b/server/__main__.py index 133122c..ae0a2d9 100644 --- a/server/__main__.py +++ b/server/__main__.py @@ -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