Skip to content
GitHub

ADR-002: Pool Management

Aceptada - 2024 (implementada en Mother container)


El sistema multi-tenant Nostromo (ADR-001) requiere conexiones PostgreSQL eficientes:

  • Múltiples tenants: Sistema soporta decenas de clientes simultáneos
  • Conexiones costosas: Crear conexión PostgreSQL toma ~50-100ms
  • Concurrencia alta: Múltiples requests simultáneos por tenant
  • Recursos limitados: PostgreSQL tiene límite de conexiones (~100-200 typical)

Con schema-per-tenant, cada request necesita:

  1. Conexión a PostgreSQL
  2. Switch a schema correcto (SET search_path)
  3. Ejecutar query
  4. Liberar conexión

Sin pooling: Crear/destruir conexión por request = latencia inaceptable + agotamiento de conexiones.


Implementar connection pool central con schema switching:

[Orchestrator Nodes]
[Mother Container - PgBouncer/Pool]
[PostgreSQL Database]
├── schema: tenant_empresa_123
├── schema: tenant_empresa_456
└── schema: central

Mother Container (c:\dev\Accounting\orchestrator\src\lib\db\pool.ts):

  • Pool central de conexiones (pg-pool)
  • Configuración: max: 50, idleTimeoutMillis: 30000

getTenantPool() function:

async function getTenantPool(tenantId: string) {
const pool = await getPool(); // Pool global
const client = await pool.connect();
// Switch schema
await client.query(`SET search_path TO tenant_${tenantId}`);
return client;
}

Flujo por request:

  1. Request llega a Orchestrator
  2. Resolve tenant desde JWT → tenantId
  3. getTenantPool(tenantId) obtiene conexión del pool
  4. Ejecuta SET search_path automáticamente
  5. Ejecuta queries (ya en schema correcto)
  6. Libera conexión al pool (schema se resetea)

Performance excelente

  • Conexiones reutilizadas (no overhead de crear/destruir)
  • Latencia típica: ~5-10ms para obtener conexión del pool
  • SET search_path es rápido (~0.1ms en PostgreSQL)

Escalabilidad

  • Pool maneja concurrencia automáticamente
  • 50 conexiones en pool soportan ~500-1000 req/s (con queries rápidas)
  • Si pool agota, requests esperan (queue) en vez de fallar

Gestión centralizada

  • Single point of configuration (max connections, timeouts)
  • Monitoring de pool health en un solo lugar
  • Facilita tuning de performance

Resource efficiency

  • 50 conexiones compartidas entre todos los tenants
  • Sin pool: necesitarías N tenants × M conexiones = explosión de recursos
  • Con pool: recursos constantes sin importar número de tenants

Compatibilidad

  • Funciona con transacciones (BEGIN/COMMIT)
  • Compatible con prepared statements
  • No requiere cambios en queries

Schema switching overhead

  • Cada request ejecuta SET search_path (~0.1ms)
  • Overhead acumulado: 0.1ms × 1000 req/s = 100ms/s overhead
  • Mitigación: Overhead es mínimo comparado con beneficio de pooling

Pool exhaustion afecta a todos

  • Si 1 tenant hace queries lentas, agota pool → otros tenants esperan
  • Mitigación: Timeouts configurados (statement_timeout, idle_in_transaction_session_timeout)
  • Mitigación: Monitoring de query time, disconnect clientes lentos

Complejidad de debugging

  • Pool oculta conexiones subyacentes
  • Difícil rastrear qué tenant usa qué conexión física
  • Mitigación: Logging de tenantId + query en cada request

Schema leakage risk

  • Si no se resetea schema entre usos, tenant A podría acceder a schema de tenant B
  • Mitigación: pg-pool resetea session state automáticamente al liberar conexión
  • Mitigación: Test de aislamiento en suite de tests

Connection limits

  • PostgreSQL tiene límite duro (ej: max_connections = 100)
  • Pool no puede exceder ese límite
  • Mitigación: PgBouncer como pool externo si se requiere más conexiones

Descripción: Cada tenant tiene su propio pool dedicado.

Ejemplo:

const pools = {
tenant_123: new Pool({ max: 10 }),
tenant_456: new Pool({ max: 10 }),
// ...
};

Rechazada porque:

  • Resource explosion: 50 tenants × 10 conexiones = 500 conexiones totales
  • Pool overhead: Gestionar 50 pools es complejo
  • Ineficiente: Tenants pequeños tienen pool ocioso, grandes saturan su pool
  • No escala: Agregar tenant = crear pool nuevo

Cuándo sería válida: Si cada tenant tiene DB dedicada (no schema-per-tenant), o requisitos de resource isolation extremo.


Opción B: Serverless connections (RDS Proxy, Neon)

Section titled “Opción B: Serverless connections (RDS Proxy, Neon)”

Descripción: Usar proxy que gestiona conexiones serverless.

Rechazada porque:

  • Vendor lock-in: RDS Proxy es AWS-only, Neon es servicio externo
  • Cold start latency: Conexiones serverless tienen latency inicial (~50-100ms)
  • Costo: Servicios serverless suelen ser más caros que self-managed pool
  • Overkill: Para aplicación con tráfico constante, pool tradicional es suficiente

Cuándo sería válida: Si deployment es serverless (AWS Lambda, Vercel Edge), o tráfico es muy esporádico (minutos/horas entre requests).


Opción C: No pooling (conexión por request)

Section titled “Opción C: No pooling (conexión por request)”

Descripción: Crear conexión nueva por cada request, destruir al terminar.

Rechazada porque:

  • Latencia inaceptable: 50-100ms por request solo en conexión
  • Agotamiento de conexiones: PostgreSQL limita a ~100-200 conexiones concurrentes
  • CPU overhead: Crear/destruir conexiones consume CPU en cliente y servidor
  • No viable para producción

Cuándo sería válida: Scripts batch con 1-2 queries totales, o ambiente dev local.


Implementación:

Decisiones relacionadas:

Referencias externas:


Pool Size: max: 50

  • Basado en: ~500-1000 req/s esperados, queries promedio ~10-50ms
  • Formula aproximada: pool_size = expected_rps × avg_query_time
  • Tuning futuro según métricas reales

Timeouts:

{
connectionTimeoutMillis: 5000, // Esperar max 5s por conexión
idleTimeoutMillis: 30000, // Cerrar conexión idle después 30s
max: 50, // Máximo 50 conexiones
min: 10 // Mínimo 10 conexiones (warm)
}

Métricas actuales:

  • Pool size (active/idle/waiting)
  • Query duration por tenant
  • Connection errors (timeouts, pool exhaustion)

Dashboards:

  • Grafana panel con pool health
  • Alertas si waiting_clients > 10 (pool bajo presión)

Lecciones aprendidas:

  1. min: 10 mantiene warm pool: Evita cold start en requests iniciales
  2. statement_timeout: 30s: Previene queries runaway que agoten pool
  3. Connection recycling: pg-pool recicla conexiones automáticamente cada hora (previene leaks)

Si pool actual no es suficiente:

  1. PgBouncer: Proxy externo, soporta miles de conexiones virtuales
  2. Read replicas: Queries SELECT van a réplica, solo writes a primary
  3. Sharding: Distribuir tenants a múltiples PostgreSQL servers

Trigger para upgrade: Si waiting_clients promedio > 20 durante horas pico.