Clase 061 — Grupo de conexiones#
⬅️ 060 · 📚 Parte 4 · 🎓 Clases · 062 ➡️ Parte 4 — Datos · Nivel 🔴 avanzado · Pista
datos✅ Clase construida — 2 implementaciones verificadas contracontrato.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 |
sí: size(), checkedout() |
| Hibernate (Spring Boot) | HikariCP | sí: 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.
- Documentación oficial: https://docs.sqlalchemy.org/
- Estado en el catálogo: activo
- Versión que ejecuta esta clase:
fastapi==0.121.3, uvicorn==0.40.0, sqlalchemy==2.0.44 - Necesita en el PATH:
python
Arrancarla suelta, sin el verificador:
PORT=3000 python -m uvicorn main:app --host 127.0.0.1 --port 3000Qué 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.
- Documentación oficial: https://hibernate.org/orm/documentation/
- Estado en el catálogo: activo
- Versión que ejecuta esta clase:
spring-boot 3.5.6, Java 21, spring-boot-starter-web, spring-boot-starter-data-jpa, h2 - Necesita en el PATH:
java,mvn
Preparar sus dependencias, dentro de su directorio:
mvn -q -B package -DskipTestsArrancarla suelta, sin el verificador:
PORT=3000 java -jar target/clase-061-1.0.0.jar --server.port=3000Qué 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.mjsdice 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=2spring.datasource.hikari.connection-timeout=1000El 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: 0 — prestada, 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:
- Son finitas. Siempre. El número por omisión es pequeño.
- Hay que devolverlas. Si no, el grupo encoge para siempre.
- Cuando no queda ninguna, alguien espera. Y la espera tiene que tener fin.
⚠️ Esperar sin límite es peor que fallar#
pool_timeout=1 # SQLAlchemyspring.datasource.hikari.connection-timeout=1000Sin 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 devueltaEs 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:
- Una excepción entre pedir y devolver, sin
finally. - Un
Connectionobtenido a mano y no cerrado. - Un resultado en flujo cuya conexión se cierra al terminar de leerlo — y nadie termina de leerlo.
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:
- El límite útil lo marca el servidor, no tu aplicación. Si tienes diez instancias con veinte conexiones cada una, le estás pidiendo doscientas sesiones a la base — y ese número suele estar por encima de lo que acepta.
- La cola es información. Si
en_usoroza el máximo de forma sostenida, el problema no es el tamaño del grupo: son las consultas, que tardan demasiado y retienen su conexión más de lo debido.
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#
- No poner límite de espera. Convierte lentitud en caída.
- Pedir una conexión fuera de un bloque con cierre garantizado.
- Agrandar el grupo ante la contención. Casi siempre empeora.
- No contar las instancias. El límite es del servidor, no del proceso.
- Hacer llamadas de red con una conexión prestada. La retiene durante toda la espera, para nada.
- Abrir la conexión antes de necesitarla. Sobre todo antes de validar la entrada: una petición inválida no debería consumir el recurso escaso.
✅ 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#
- Por qué sí y por qué no
- Clase 051 — Conectar a una base de datos
- Clase 032 — Tiempos de espera
- Módulo 06 — Persistencia y dominio
Fuentes#
- [nygard-release-it] Nygard, Michael T. Release It!, 2.ª ed. Pragmatic Bookshelf, 2018. ISBN 9781680502398 — https://openlibrary.org/isbn/9781680502398
- [gregg-systems-performance] Gregg, Brendan. Systems Performance, 2.ª ed. Addison-Wesley, 2020. ISBN 9780136820154 — https://openlibrary.org/isbn/9780136820154
- [kleppmann-ddia] Kleppmann, Martin. Designing Data-Intensive Applications. O'Reilly Media, 2017. ISBN 9781449373320 — https://openlibrary.org/isbn/9781449373320