Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 061 — Grupo de conexiones#

⬅️ 060 · 📚 Parte 4 · 🎓 Clases · 062 ➡️ Parte 4 — Datos · Nivel 🔴 avanzado · Pista datosClase construida — 2 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Entender el recurso escaso que hay detrás de cada consulta, y qué pasa cuando se acaba.

🎬 Por qué el elenco es de dos#

El grupo de conexiones no es del ORM: es del controlador. Prisma y Entity Framework Core aparecían en el elenco original de esta clase y se han quitado por una razón concreta y verificable:

ORM Grupo ¿Se puede observar?
SQLAlchemy QueuePool propio : size(), checkedout()
Hibernate (Spring Boot) HikariCP : HikariPoolMXBean
Prisma del motor de consultas, connection_limit no expone el número prestado
EF Core con SQLite del proveedor, sin ajustes no

Meterlos igualmente habría exigido simular el grupo con un semáforo, y una simulación no enseña el comportamiento real — enseña el que le programaste. Es la misma decisión que toma cada clase de este laboratorio: el elenco es la lista de frameworks para los que el problema existe de verdad.

🧩 La situación#

Un grupo de dos conexiones. Tres peticiones a la vez. Y una fuga.

<!-- generado: fichas -->

📖 Las palabras que esta clase define#

Si alguna de estas no te dice nada todavía, esta es la clase donde se aprende. Las definiciones viven en el glosario, que reúne las del programa entero.

Palabra Qué significa
Grupo de conexiones (Pool) Un conjunto de conexiones a la base de datos que se reutilizan en lugar de abrirse y cerrarse por petición. Abrir una conexión es caro; el tamaño del grupo es un límite duro de concurrencia que casi nadie mira hasta que se agota.

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
SQLAlchemy mapeador objeto-relacional de Python (Python) 2006 MIT proyecto independiente
Hibernate ORM mapeador objeto-relacional de JVM (Java) 2001 LGPL-2.1-or-later proyecto independiente

🔧 SQLAlchemy#

Separa explícitamente el constructor de consultas del mapeador, de modo que se puede bajar de nivel sin abandonarlo.

Arrancarla suelta, sin el verificador:

PORT=3000 python -m uvicorn main:app --host 127.0.0.1 --port 3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
main.py código Python
requirements.txt dependencias de Python, una por línea, con versión fijada

🔧 Hibernate ORM#

El mapeador objeto-relacional de referencia en Java y el origen de buena parte del vocabulario del campo, incluido el problema de la consulta N+1.

Preparar sus dependencias, dentro de su directorio:

mvn -q -B package -DskipTests

Arrancarla suelta, sin el verificador:

PORT=3000 java -jar target/clase-061-1.0.0.jar --server.port=3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
pom.xml manifiesto de Maven: el proyecto, su Java, sus dependencias y cómo se empaqueta
src/main/java/labs/Aplicacion.java código Java
src/main/resources/application.properties configuración de Spring Boot: lo que se ajusta sin tocar el código

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Las dos exponen el número de conexiones prestadas en cada momento, y las dos fallan por el mismo sitio cuando se acaban. Lo importante es que el grupo ya estaba ahí antes de esta clase: en las clases 053-060 nadie lo configuró y funcionó igual. Aquí solo lo hacemos pequeño para poder verlo.

SQLAlchemy · sqlalchemy/main.py#

El grupo, declarado:

motor = create_engine(
    "sqlite:///datos.db",
    poolclass=QueuePool,
    pool_size=2,
    max_overflow=0,
    pool_timeout=1,
)

Dos conexiones a propósito. Con el valor por omisión —cinco, más diez de desbordamiento— harían falta dieciséis peticiones simultáneas para ver algo, y la clase trata justamente de que este número es pequeño y finito.

max_overflow=0 quita el colchón: el grupo es dos y no hay más. pool_timeout=1 es la diferencia entre fallar y quedarse colgado.

El dato del panel:

    return JSONResponse({"tamano": motor.pool.size(), "en_uso": motor.pool.checkedout()})

checkedout() es el número de conexiones prestadas ahora mismo. Si sube y no baja, hay una fuga. Si roza el máximo de forma sostenida, el grupo está mal dimensionado. Es la métrica que conviene tener a la vista antes de que el servicio se pare.

Prestada, no regalada:

    with motor.connect() as conexion:
        conexion.execute(text("SELECT COUNT(*) FROM tareas")).scalar_one()
    return JSONResponse({"ok": True})

Ese with devuelve la conexión al grupo al salir — también si la consulta lanza una excepción. Toda la diferencia entre un servicio que aguanta y uno que se para a la hora cabe en esas cuatro letras.

Esperar y entrar:

def _retener(segundos: float, esperas: list[float], barrera: threading.Barrier) -> None:
    barrera.wait()
    inicio = time.monotonic()
    with motor.connect() as conexion:
        esperas.append(time.monotonic() - inicio)

La barrera hace que los tres hilos pidan a la vez; el cronómetro empieza antes de pedir y para al conseguirla. Así la espera de la tercera es un número, no una impresión: el contrato exige espero_alguna: true.

Y fallar:

    except TiempoAgotado:
        respuesta = JSONResponse({"code": "GRUPO_AGOTADO"}, status_code=503)

Un 503 con código, no un cuelgue. El cliente sabe que puede reintentar. Es la misma decisión de la clase 020 aplicada a un recurso interno.

La fuga:

    fugadas.append(motor.connect())
    return JSONResponse({"fugadas": len(fugadas)})

Pedir prestado y no devolver. No hay excepción, no hay registro, no hay nada: el grupo simplemente tiene una conexión menos para siempre. Se guarda en una lista precisamente para que el recolector de basura no la cierre por su cuenta — así la fuga es real y en_uso se queda en 1.

Hibernate + HikariCP · hibernate/…/Aplicacion.java#

En Spring Boot el grupo es HikariCP y está puesto sin que nadie lo pida — es el mismo autoconfigurado de todas las clases anteriores. Aquí solo se le cambian dos números, en application.properties:

spring.datasource.hikari.maximum-pool-size=2
spring.datasource.hikari.minimum-idle=2
spring.datasource.hikari.connection-timeout=1000

El dato del panel:

            return Map.of(
                    "tamano", fuente.getHikariConfigMXBean().getMaximumPoolSize(),
                    "en_uso", fuente.getHikariPoolMXBean().getActiveConnections());

Hikari publica sus contadores por JMX, así que ese número no hay que llevarlo a mano: Micrometer lo exporta a Prometheus con una línea de configuración.

Prestada, no regalada — sin with a la vista:

        @GetMapping("/consulta")
        public Map<String, Object> consulta() {
            tareas.count();
            return Map.of("ok", true);
        }

Aquí no se ve ninguna conexión. El repositorio la pide, la usa y la devuelve al cerrar la transacción. Es cómodo y es peligroso a la vez: cuando el préstamo es invisible, la fuga también lo es hasta que alguien mira el panel.

            try (Connection conexion = fuente.getConnection()) {

Cuando sí se baja al DataSource, el try con recursos hace el mismo trabajo que el with de Python: devolver pase lo que pase.

Y la fuga, otra vez sin ruido:

            fugadas.add(fuente.getConnection());
            return Map.of("fugadas", fugadas.size());

Con una diferencia a favor de Hikari, y merece citarse porque es la razón de usarlo: sabe avisar de esto. spring.datasource.hikari.leak-detection-threshold registra un aviso cuando una conexión lleva demasiado tiempo fuera, con la traza de quién la pidió. Está desactivado por omisión; encenderlo en producción es de las configuraciones más rentables que existen.

🧮 El contrato#

Petición Respuesta
GET /grupo tamano: 2, en_uso: 0
GET /consulta ok
GET /grupo en_uso: 0prestada, no regalada
GET /tres-a-la-vez completadas: 3, espero_alguna: true
GET /agotar 503 GRUPO_AGOTADO
GET /grupo en_uso: 0 — se recuperó solo
GET /fugar fugadas: 1
GET /grupo en_uso: 1 — y ahí se queda

📖 Por qué existe el grupo#

Abrir una conexión no es abrir un archivo. Es un saludo TCP, una autenticación, a veces una negociación TLS, y memoria reservada en el servidor de base de datos para esa sesión. Decenas de milisegundos, y un coste que se paga también al otro lado.

Por eso se reutilizan. El grupo mantiene unas cuantas abiertas y te presta una mientras la necesitas.

Y de ahí salen las tres propiedades que definen esta clase:

  1. Son finitas. Siempre. El número por omisión es pequeño.
  2. Hay que devolverlas. Si no, el grupo encoge para siempre.
  3. Cuando no queda ninguna, alguien espera. Y la espera tiene que tener fin.

⚠️ Esperar sin límite es peor que fallar#

pool_timeout=1     # SQLAlchemy
spring.datasource.hikari.connection-timeout=1000

Sin ese límite, una petición que no consigue conexión se queda colgada. Y mientras espera retiene su propio hilo, que también es un recurso finito.

El resultado es un fallo en cascada: la base va lenta → las peticiones esperan → los hilos se agotan → el servidor deja de aceptar peticiones nuevas, incluida la de comprobación de salud. Nygard le puso nombre: los recursos integrados —hilos, conexiones, sockets— se agotan en cadena, y el sistema deja de responder mucho antes de que la base de datos se caiga [nygard-release-it].

Un 503 a tiempo, en cambio, libera el hilo, le dice al cliente que reintente y deja el servicio contestando.

⚠️ La fuga#

fugadas.append(motor.connect())   # pedida y nunca devuelta

Es el fallo más difícil de encontrar de esta clase, porque no hay error. No falla nada, no se registra nada. El grupo simplemente tiene una conexión menos, y el síntoma llega horas después: «la aplicación se cuelga por las tardes».

Las causas habituales son tres, y las tres son omisiones:

La defensa está en el lenguaje, no en la disciplina: with en Python, try-with-resources en Java, using en C#. Si el préstamo no está dentro de un bloque, es una fuga esperando a ocurrir.

Y HikariCP sabe avisar: leakDetectionThreshold registra un aviso cuando una conexión lleva demasiado tiempo fuera. Vale la pena tenerlo puesto.

🔬 Qué tamaño debe tener el grupo#

La intuición dice «más grande, más rápido». Es falsa.

Cada conexión activa es trabajo real en el servidor de base de datos, que tiene un número limitado de núcleos y discos. Pasado cierto punto, más conexiones solo añaden contención: el mismo trabajo repartido en más piezas que se pelean.

Dos observaciones prácticas:

De ahí que el dato que hay que vigilar no sea el tamaño, sino cuánto tiempo se espera para conseguir una.

🔬 Comparación#

SQLAlchemy Hibernate (HikariCP)
Tamaño pool_size + max_overflow maximum-pool-size
Espera máxima pool_timeout connection-timeout
Prestadas ahora pool.checkedout() getActiveConnections()
Detección de fugas pool_pre_ping, registro leakDetectionThreshold
Al agotarse TimeoutError SQLTransientConnectionException

La diferencia de diseño que merece atención es max_overflow: SQLAlchemy separa el tamaño estable del colchón temporal. Un grupo de cinco con diez de desbordamiento mantiene cinco abiertas y abre hasta diez más en un pico, cerrándolas después. Hikari tiene un solo número, y la elasticidad la da minimum-idle.

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 061

🧪 Reto de transferencia#

Llama a /fugar dos veces y después a /consulta. Con el grupo de dos, la segunda fuga lo deja a cero y la consulta tarda un segundo y falla — sin que nada en el código de /consulta haya cambiado. Ese salto entre «funciona» y «no funciona» sin ninguna modificación es la forma exacta en que este fallo se vive en producción.

🔗 Enlaces#

Fuentes#