ADR-031 · DB universal access pattern · pgbouncer + middleware métrico + psql-api expansión¶
Fecha: 2026-05-04 Source:
/srv/projects/cis/cis-plan/DECISIONS.md(do not edit here — re-split desde la fuente)
Contexto
Hoy múltiples servicios CIS conectan directo a Postgres :5432 (asyncpg/sqlalchemy). PgBouncer instalado en :6432 (transaction mode, pool_size=20, max_client_conn=200) pero no usado universalmente. cis-core expone /api/v1/postgresql/{databases,users,grant} (admin endpoints) pero no hay query API uniforme.
CONSTITUTION §2.1 ya prescribe "PgBouncer transaction mode + statement_cache_size=0" como canon, pero hay drift: services nuevos creados desde cis-core fork siguen usando :5432 directo.
Decisión
- Acceso uniforme: TODOS los servicios CIS (Python ORM, scripts, agentes) acceden a Postgres vía pgbouncer
:6432. El:5432queda solo para cis-core admin endpoints (vacuum, CREATE DATABASE, GRANT) y backups pg_dump. - Middleware métrico: cada servicio FastAPI/SQLAlchemy expone instrumentación
db_query_seconds_total{service,database,operation}Prometheus +db_query_count_total+db_query_bytes_total. Implementación:core-db-basepaquete (ya existe) extendido con middleware async wrapper. - psql-api expansión: cis-core agrega
/api/v1/db/{database}/query(POST SQL + params, read-only,auto) y/api/v1/db/{database}/exec(writes, FES-gated por ADR-026). Para queries ad-hoc sin necesidad de cliente Python. - Cutover obligatorio: cualquier nuevo servicio nace en pgbouncer :6432. Servicios legacy migran cuando reciban refactor. Excepción documentada: pg_dump/restore que necesita session-mode.
Alternativas descartadas
- Acceso directo :5432 universal: pierde connection pooling, max_connections explota con N services × M workers.
- Solo HTTP via psql-api: latency overhead inaceptable para hot paths (ORM queries).
- Cliente Postgres custom: re-implementar SQLAlchemy es overkill.
Consecuencias positivas
- Connection pooling efectivo: 200 connections máx total vs 20× services creando connections individuales.
- Métricas centralizadas: Grafana dashboard "DB performance per service" con tres time series (latencia, count, bytes).
- Query API ad-hoc para Claudia (
/sudoMCP) sin necesidad de psycopg en su context.
Consecuencias negativas / gotchas
- asyncpg + pgbouncer transaction mode requiere
statement_cache_size=0(ya documentado §10.5 CONSTITUTION). - prepared statements no funcionan cross-connection en transaction mode.
- LISTEN/NOTIFY no funciona en transaction mode (usar Redis pub/sub si necesario).
Implementación
- agent-DB-bouncer-cutover (este sprint): cambiar DATABASE_URL en cada servicio :5432 → :6432, restart, verify.
- core-db-base v0.2.0: middleware Prometheus + helper
get_engine(metric_name). - cis-core /api/v1/db/* endpoints (delegar a core-db-base).
Verificación
ss -tn | grep ESTAB | grep :5432debe mostrar solo cis-core admin + pg_dump tooling.ss -tn | grep ESTAB | grep :6432muestra todas las app connections.curl http://127.0.0.1:8200/metrics | grep db_query_seconds_totalmuestra series por service.
Relaciona
- ADR-026 (operations permission model:
/api/v1/db/{database}/execes FES-gated). - CONSTITUTION §2.1 (pgbouncer canon, ahora enforced).
- core-db-base v0.1.0 (P0.5 cierre, base).