Tests que fallan al azar: el pool contra max_connections
Una suite de pruebas que falla siempre en el mismo sitio es una buena noticia: señala el problema. Una que falla al azar, en un archivo distinto cada vez, es de las peores situaciones posibles. Eso empezó a pasar al agregar un archivo de pruebas nuevo, y todo apuntaba a que el código recién escrito había roto algo. No era así.
El síntoma
Las pruebas usan el runner nativo de Node.js y una base de datos PostgreSQL real. Tras sumar un archivo, la suite comenzó a fallar con errores de conexión en archivos que no tenían relación con el cambio. Al volver a correr, fallaban otros. Quitar el archivo nuevo lo arreglaba, lo que reforzaba la sospecha equivocada.
La causa: aritmética
El runner de pruebas de Node ejecuta cada archivo en su propio proceso. Cada proceso importa el módulo de base de datos, y ese módulo crea su propio pool de conexiones. Con un pool de hasta 10 conexiones y 11 archivos de prueba en paralelo, el total posible es de 110 conexiones. PostgreSQL, con su configuración por defecto, acepta 100.
Mientras había 10 archivos, el máximo teórico era justo 100 y casi nunca se alcanzaba. El archivo número 11 cruzó el umbral. Cuál proceso se quedaba sin conexión dependía de la velocidad de cada uno en esa corrida, y por eso el fallo cambiaba de lugar.
Se cruza el umbral al agregar un archivo, así que parece que lo rompió el último cambio. Es la forma más engañosa de fallar.
Cómo confirmarlo
Antes de culpar al código, basta con consultar el límite del servidor
con SHOW max_connections y multiplicar el tamaño del pool
por la cantidad de archivos de prueba. Si el resultado se acerca o supera
el límite, ya hay un sospechoso fuerte. Mirar la vista de actividad de
PostgreSQL durante la corrida lo termina de confirmar.
La corrección sin dependencias
La solución es usar un pool más pequeño cuando el código corre dentro de las pruebas. La pregunta es cómo saberlo de forma confiable. Depender de que alguien exporte una variable de entorno antes de correr la suite es frágil, y en Windows requiere herramientas extra para definirla en el mismo comando.
Node lo resuelve solo: en cada proceso hijo que lanza el runner de
pruebas define la variable NODE_TEST_CONTEXT. El módulo de
base de datos puede leerla y reducir el pool:
const MAX_POOL = process.env.NODE_TEST_CONTEXT || env.NODE_ENV === 'test' ? 4 : 10; Con 4 conexiones por archivo, once archivos suman 44, muy lejos del límite, y quedan margen para crecer la suite. En producción el pool sigue siendo el mismo.
Otras opciones y por qué no
- Subir el límite de PostgreSQL: cada conexión consume memoria en el servidor, y solo posterga el problema hasta el archivo siguiente.
- Correr los archivos en serie: resuelve el límite pero hace la suite varias veces más lenta.
- Un pool compartido entre procesos: requiere un intermediario de conexiones, que agrega infraestructura para un problema de configuración.
La lección general
Cuando un fallo es intermitente y cambia de lugar, conviene sospechar de recursos compartidos antes que de la lógica: conexiones, puertos, archivos temporales o límites del sistema operativo. Y cuando un cambio pequeño "rompe" algo lejano, preguntarse si ese cambio solo empujó un contador por encima de un límite que ya estaba cerca.