Saltar a contenido

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

  1. Acceso uniforme: TODOS los servicios CIS (Python ORM, scripts, agentes) acceden a Postgres vía pgbouncer :6432. El :5432 queda solo para cis-core admin endpoints (vacuum, CREATE DATABASE, GRANT) y backups pg_dump.
  2. 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-base paquete (ya existe) extendido con middleware async wrapper.
  3. 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.
  4. 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 (/sudo MCP) 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 :5432 debe mostrar solo cis-core admin + pg_dump tooling.
  • ss -tn | grep ESTAB | grep :6432 muestra todas las app connections.
  • curl http://127.0.0.1:8200/metrics | grep db_query_seconds_total muestra series por service.

Relaciona

  • ADR-026 (operations permission model: /api/v1/db/{database}/exec es FES-gated).
  • CONSTITUTION §2.1 (pgbouncer canon, ahora enforced).
  • core-db-base v0.1.0 (P0.5 cierre, base).