mariadb-pool-cuenti
Version:
Gestor inteligente de pools de conexiones para MariaDB/MySQL que maneja múltiples bases de datos de empresas (multi-tenant) de forma dinámica y eficiente
908 lines (907 loc) • 42.1 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import mariadb from "mariadb";
import dotenv from "dotenv";
dotenv.config();
// Decorar conexión con métodos útiles
function decorateConnection(conn) {
const extended = conn;
extended.queryFormat = function (query, values = {}) {
if (!values)
return query;
return query.replace(/\:(\w+)/g, (txt, key) => {
if (values.hasOwnProperty(key)) {
return this.escape(values[key]);
}
return txt;
});
};
extended.query2 = function (query_1) {
return __awaiter(this, arguments, void 0, function* (query, values = {}) {
const formatted = extended.queryFormat(query, values);
return this.query(formatted);
});
};
// Liberación segura de conexiones
extended.safeRelease = function () {
return __awaiter(this, void 0, void 0, function* () {
var _a;
try {
if (((_a = this.isValid) === null || _a === void 0 ? void 0 : _a.call(this)) !== false) {
this.release();
}
}
catch (error) {
console.error("[CONNECTION] Error al liberar conexión:", error);
try {
yield this.destroy();
}
catch (destroyError) {
console.error("[CONNECTION] Error al destruir conexión:", destroyError);
}
}
});
};
return extended;
}
class DBManager {
constructor() {
this.listaServer = [];
this.pool_bases = null;
this.isInitialized = false;
this.cleanupTimer = null;
// Mapa de conexiones activas por empresa para poder desconectarlas explícitamente
this.connections = new Map();
// Inicializar pool base inmediatamente
this.initBasePool();
// Configurar limpieza periódica
this.setupCleanupTimer();
// Configurar manejo de señales para cierre seguro
this.setupGracefulShutdown();
}
static getInstance() {
if (!this.instance)
this.instance = new DBManager();
return this.instance;
}
// Método estático para verificar si el cierre está en progreso
static isShuttingDown() {
return this.shutdownInProgress;
}
// Método estático para establecer el estado de cierre (para uso del API público)
static setShuttingDown(value) {
this.shutdownInProgress = value;
}
setupCleanupTimer() {
// Limpiar conexiones inactivas cada 5 minutos
this.cleanupTimer = setInterval(() => {
this.cleanupStaleConnections().catch(err => {
console.error("[CLEANUP] Error en limpieza programada:", err);
}).then(result => {
if (result && (result.closed > 0 || result.leaksDetected > 0)) {
console.log(`[CLEANUP] Limpieza completada: ${result.closed} conexiones cerradas, ${result.leaksDetected} fugas detectadas`);
}
});
}, 5 * 60 * 1000); // Cada 5 minutos
// No mantener el proceso vivo solo por este timer
this.cleanupTimer.unref();
// Ejecutar una primera vez para detección temprana de problemas
setTimeout(() => {
this.cleanupStaleConnections().catch(err => {
console.error("[CLEANUP] Error en primera limpieza:", err);
});
}, 60 * 1000); // Después de 1 minuto
}
setupGracefulShutdown() {
// Función para manejar la terminación de manera segura
const handleTermination = (signal) => __awaiter(this, void 0, void 0, function* () {
if (DBManager.shutdownInProgress) {
console.log(`[SHUTDOWN] Ya se está ejecutando el cierre, ignorando ${signal}`);
return;
}
DBManager.shutdownInProgress = true;
console.log(`[SHUTDOWN] Señal ${signal} recibida, iniciando cierre seguro...`);
try {
// Timeout para garantizar terminación - asegurarse de que siempre termine
const shutdownTimeout = setTimeout(() => {
console.error("[SHUTDOWN] Timeout de cierre alcanzado, forzando terminación");
// Forzar salida inmediata
process.exit(1);
}, 10000); // 10 segundos para dar tiempo suficiente a las operaciones de cierre
// Para evitar que el timeout mantenga el proceso vivo
shutdownTimeout.unref();
// Ejecutar cleanup
yield this.cleanup();
// Limpiar timeout
clearTimeout(shutdownTimeout);
console.log("[SHUTDOWN] Cierre completado exitosamente");
// Para señales de terminación, forzar salida después de un breve retraso
// Esto garantiza que el proceso no se quede como zombie
if (signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGQUIT') {
console.log(`[SHUTDOWN] Señal ${signal} manejada, terminando proceso en 100ms`);
// Dar 100ms para que cualquier operación pendiente termine
setTimeout(() => {
// Usar código de estado 0 (éxito) para salida normal
process.exit(0);
}, 100);
}
}
catch (error) {
console.error("[SHUTDOWN] Error durante el cierre:", error);
process.exit(1);
}
});
// Registro único de manejadores de señales (usando once para evitar duplicados)
const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
signals.forEach(signal => {
// Eliminar manejadores existentes para evitar duplicados
process.removeAllListeners(signal);
// Registrar nuevo manejador una sola vez
process.once(signal, () => handleTermination(signal));
});
// Manejar errores no capturados una sola vez
process.removeAllListeners('uncaughtException');
process.once("uncaughtException", (error) => __awaiter(this, void 0, void 0, function* () {
console.error("[SHUTDOWN] Error no capturado:", error);
yield handleTermination("uncaughtException");
}));
process.removeAllListeners('unhandledRejection');
process.once("unhandledRejection", (reason) => __awaiter(this, void 0, void 0, function* () {
console.error("[SHUTDOWN] Promesa rechazada no capturada:", reason);
try {
// Envolver en try-catch para evitar ciclos recursivos de unhandledRejection
yield handleTermination("unhandledRejection");
}
catch (err) {
console.error("[SHUTDOWN] Error en manejador de unhandledRejection:", err);
process.exit(1);
}
}));
console.log("[SHUTDOWN] Manejadores de terminación configurados");
}
initBasePool() {
if (this.isInitialized)
return;
console.log("[BASE] Iniciando pool de conexiones principal...");
const environment = process.env.ENVIRONMENT_DATA_BASE;
// Configuración de pool más conservadora para evitar proliferación de conexiones
const commonConfig = {
acquireTimeout: 60000,
connectionLimit: 5,
idleTimeout: 60000, // Cerrar conexiones inactivas después de 1 minuto
minimumIdle: 1, // Mantener al menos 1 conexión
maxRetries: 2,
leakDetectionTimeout: 120000
};
if (environment === "dev") {
this.pool_bases = mariadb.createPool(Object.assign(Object.assign({ host: process.env.DB_HOST_DEV, user: process.env.DB_USER_DEV, password: process.env.DB_PWD_DEV, database: process.env.DATABASE_DEV, port: parseInt(process.env.DB_PORT_DEV || "3306") }, commonConfig), { connectionLimit: parseInt(process.env.POOL_SIZE_BASE_DEV || "5") }));
}
else {
this.pool_bases = mariadb.createPool(Object.assign(Object.assign({ host: process.env.DB_HOST_PRO, user: process.env.DB_USER_PRO, password: process.env.DB_PWD_PRO, database: process.env.DATABASE_PRO, port: parseInt(process.env.DB_PORT_PRO || "3306") }, commonConfig), { connectionLimit: parseInt(process.env.POOL_SIZE_BASE_PRO || "5") }));
}
this.pool_bases.on("connection", (conn) => console.log(`[BASE] Conexión ${conn.threadId} creada en pool principal`));
this.pool_bases.on("release", (conn) => console.log(`[BASE] Conexión ${conn.threadId} liberada del pool principal`));
this.isInitialized = true;
console.log("[BASE] Pool principal inicializado correctamente");
}
getPoolBases() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.isInitialized) {
this.initBasePool();
}
if (!this.pool_bases) {
throw new Error("Pool de base de datos principal no inicializado");
}
try {
const conn = yield this.pool_bases.getConnection();
const extended = decorateConnection(conn);
// Registrar la conexión en un ID especial para el pool base (0)
this.trackConnection(0, extended);
return extended;
}
catch (error) {
console.error("[BASE] Error obteniendo conexión:", error);
throw error;
}
});
}
consultaServidores(idEmpresa, idAplicacion) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
// Variable estática para guardar la conexión entre llamadas
if (!this.__configConnection) {
this.__configConnection = null;
}
let conn = this.__configConnection;
let isNewConnection = false;
try {
// Intentar reutilizar la conexión existente
if (!conn || !((_b = (_a = conn).isValid) === null || _b === void 0 ? void 0 : _b.call(_a))) {
conn = yield this.getPoolBases();
this.__configConnection = conn;
isNewConnection = true;
}
const sql = `SELECT s.es_proxy,e.id_servidor,e.nombreBaseDatos,ip,puerto,usuario,
clave,initialSize,maxActive,maxIdle,maxTiempoInatividad
FROM aplicaciones_empresa e
INNER JOIN Servidores s ON(e.id_servidor=s.id)
WHERE e.id_empresa=? AND e.id_aplicacion=?`;
const rows = yield conn.query(sql, [idEmpresa, idAplicacion]);
if (rows.length > 0) {
return rows[0];
}
else {
throw new Error(`Aplicacion no generada para empresa ${idEmpresa}`);
}
}
catch (error) {
console.error(`[CONFIG] Error consultando servidor:`, error);
// Si hay error, invalidar la conexión para crear una nueva en la próxima llamada
if (conn === this.__configConnection) {
this.__configConnection = null;
}
throw error;
}
finally {
// Solo liberar si es una conexión nueva y hubo error o aleatoriamente
if (conn && (isNewConnection || Math.random() > 0.9)) {
try {
yield conn.safeRelease();
if (conn === this.__configConnection) {
this.__configConnection = null;
}
}
catch (err) {
console.error(`[CONFIG] Error liberando conexión:`, err);
this.__configConnection = null;
}
}
}
});
}
getConnectionEmpresa(idEmpresa, _idAplicacion) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.isInitialized) {
this.initBasePool();
}
const idAplicacion = parseInt(process.env.ID_APPLICATION || "1");
let indiceServer = -1;
let indiceEmpresa = -1;
// Buscar servidor existente
for (let i = 0; i < this.listaServer.length; i++) {
for (let j = 0; j < this.listaServer[i].lstEmpresas.length; j++) {
if (this.listaServer[i].lstEmpresas[j].id_empresa === idEmpresa) {
indiceServer = i;
indiceEmpresa = j;
break;
}
}
if (indiceServer !== -1)
break;
}
if (indiceServer === -1) {
// Consultar configuración del servidor
const objServer = yield this.consultaServidores(idEmpresa, idAplicacion);
// Buscar si el servidor ya existe
for (let i = 0; i < this.listaServer.length; i++) {
if (this.listaServer[i].id_servidor === objServer.id_servidor) {
indiceServer = i;
break;
}
}
if (indiceServer > -1) {
// Registrar empresa en servidor existente
this.listaServer[indiceServer].lstEmpresas.push({
id_empresa: idEmpresa,
nombreBaseDatos: objServer.nombreBaseDatos,
});
// Buscar conexión recursivamente
return yield this.getConnectionEmpresa(idEmpresa, idAplicacion);
}
else {
// Crear nuevo servidor
const environment = process.env.ENVIRONMENT_DATA_BASE;
objServer.maxActive = parseInt(environment === "dev"
? process.env.POOL_SIZE_BASE_DEV || "5"
: process.env.POOL_SIZE_BASE_PRO || "5");
let pool = null;
if (objServer.es_proxy === 0) {
pool = mariadb.createPool({
host: objServer.ip,
user: objServer.usuario,
password: objServer.clave,
connectionLimit: objServer.maxActive,
port: objServer.puerto,
acquireTimeout: 60000,
idleTimeout: 60000, // Cerrar conexiones inactivas después de 1 minuto
minimumIdle: 1, // Mantener al menos 1 conexión
leakDetectionTimeout: 180000
});
pool.on("connection", (conn) => console.log(`[TENANT] Conexión ${conn.threadId} creada en pool servidor ${objServer.id_servidor}`));
pool.on("release", (conn) => console.log(`[TENANT] Conexión ${conn.threadId} liberada del pool servidor ${objServer.id_servidor}`));
}
this.listaServer.push({
id_servidor: objServer.id_servidor,
ip: objServer.ip,
puerto: objServer.puerto,
usuario: objServer.usuario,
clave: objServer.clave,
initialSize: objServer.initialSize,
maxActive: objServer.maxActive,
maxIdle: objServer.maxIdle,
maxTiempoInatividad: objServer.maxTiempoInatividad,
lstEmpresas: [
{
id_empresa: idEmpresa,
nombreBaseDatos: objServer.nombreBaseDatos,
},
],
pool: pool,
es_proxy: objServer.es_proxy,
lastHealthCheck: new Date()
});
return yield this.getConnectionEmpresa(idEmpresa, idAplicacion);
}
}
else {
// Servidor encontrado, obtener conexión
const server = this.listaServer[indiceServer];
const empresa = server.lstEmpresas[indiceEmpresa];
let conn;
if (server.es_proxy === 1) {
// Conexión directa para proxy
conn = (yield mariadb.createConnection({
host: server.ip,
user: server.usuario,
password: server.clave,
database: empresa.nombreBaseDatos,
multipleStatements: true,
port: server.puerto,
}));
// Marcar como proxy para saber cómo tratarla al release
conn.__cuentiEsProxy = 1;
// Personalizar método release para conexiones directas
conn.release = function () {
return __awaiter(this, void 0, void 0, function* () {
yield this.end();
});
};
}
else {
// Conexión desde pool
if (!server.pool) {
throw new Error(`Pool no disponible para servidor ${server.id_servidor}`);
}
conn = yield server.pool.getConnection();
}
const extended = decorateConnection(conn);
this.trackConnection(idEmpresa, extended);
// Seleccionar base de datos
yield extended.query(`USE ${empresa.nombreBaseDatos}`);
return extended;
}
});
}
// Registrar conexión en el mapa para poder cerrarla explícitamente
trackConnection(idEmpresa, conn) {
var _a, _b, _c, _d, _e, _f;
// Anotar el id de empresa en la conexión
conn.__cuentiEmpresaId = idEmpresa;
// Anotar timestamp de creación
conn.__cuentiCreatedAt = new Date();
// Anotar timestamp de última actividad (igual a creación al inicio)
conn.__cuentiLastActivity = conn.__cuentiCreatedAt;
let set = this.connections.get(idEmpresa);
if (!set) {
set = new Set();
this.connections.set(idEmpresa, set);
}
set.add(conn);
// Decorar métodos para des-registrar al cerrar
const originalRelease = (_a = conn.release) === null || _a === void 0 ? void 0 : _a.bind(conn);
const originalEnd = (_b = conn.end) === null || _b === void 0 ? void 0 : _b.bind(conn);
const originalDestroy = (_c = conn.destroy) === null || _c === void 0 ? void 0 : _c.bind(conn);
const originalSafeRelease = (_d = conn.safeRelease) === null || _d === void 0 ? void 0 : _d.bind(conn);
// Decorar también el método query para actualizar la actividad
const originalQuery = (_e = conn.query) === null || _e === void 0 ? void 0 : _e.bind(conn);
const originalQuery2 = (_f = conn.query2) === null || _f === void 0 ? void 0 : _f.bind(conn);
const untrack = () => {
const current = this.connections.get(idEmpresa);
if (current) {
current.delete(conn);
if (current.size === 0) {
this.connections.delete(idEmpresa);
}
}
};
// Método para actualizar timestamp de actividad
const updateActivity = () => {
conn.__cuentiLastActivity = new Date();
};
// Asegurar que cualquier cierre también quite del tracking
if (originalRelease) {
conn.release = () => {
try {
updateActivity();
const result = originalRelease();
if (conn.__cuentiEsProxy === 1) {
untrack();
}
return result;
}
catch (error) {
untrack();
throw error;
}
};
}
if (originalSafeRelease) {
conn.safeRelease = function () {
return __awaiter(this, void 0, void 0, function* () {
try {
updateActivity();
yield originalSafeRelease();
}
catch (error) {
untrack();
throw error;
}
});
};
}
if (originalEnd) {
conn.end = () => __awaiter(this, void 0, void 0, function* () {
try {
updateActivity();
const result = yield originalEnd();
untrack();
return result;
}
catch (error) {
untrack();
throw error;
}
});
}
if (originalDestroy) {
conn.destroy = () => __awaiter(this, void 0, void 0, function* () {
try {
const result = yield originalDestroy();
untrack();
return result;
}
catch (error) {
untrack();
throw error;
}
});
}
// Actualizar el timestamp de actividad en cada query
if (originalQuery) {
conn.query = function (query, values) {
updateActivity();
return originalQuery(query, values);
};
}
if (originalQuery2) {
conn.query2 = function (query, values) {
updateActivity();
return originalQuery2(query, values);
};
}
}
// Desconecta la conexión indicada
disconnectConnection(conn) {
return __awaiter(this, void 0, void 0, function* () {
const empresaId = conn.__cuentiEmpresaId;
try {
if (typeof conn.destroy === "function") {
yield conn.destroy();
}
else if (typeof conn.end === "function") {
yield conn.end();
}
else if (typeof conn.safeRelease === "function") {
yield conn.safeRelease();
}
else if (typeof conn.release === "function") {
yield conn.release();
}
}
catch (error) {
console.error("[TENANT] Error cerrando conexión:", error);
try {
if (typeof conn.destroy === "function") {
yield conn.destroy();
}
}
catch (_) {
// ignorar
}
}
finally {
if (empresaId !== undefined) {
const set = this.connections.get(empresaId);
if (set) {
set.delete(conn);
if (set.size === 0)
this.connections.delete(empresaId);
}
}
try {
delete conn.__cuentiEmpresaId;
}
catch (_a) {
/* ignore */
}
}
});
}
// Limpiar conexiones antiguas
cleanupStaleConnections() {
return __awaiter(this, void 0, void 0, function* () {
let totalClosed = 0;
let leaksDetected = 0;
const now = new Date();
// Revisar todas las conexiones por empresa
for (const [empresaId, set] of this.connections.entries()) {
if (set.size === 0)
continue;
// Primero, verificar si hay fugas de conexiones (más de 2 minutos sin cerrar)
const leakThreshold = 2 * 60 * 1000; // 2 minutos
const potentialLeaks = Array.from(set).filter(conn => {
const timestamp = conn.__cuentiCreatedAt;
const lastActivity = conn.__cuentiLastActivity || timestamp;
if (!timestamp)
return false;
// Si tiene marca de actividad, usar esa para calcular inactividad
const timeSinceActivity = now.getTime() - ((lastActivity === null || lastActivity === void 0 ? void 0 : lastActivity.getTime()) || timestamp.getTime());
return timeSinceActivity > leakThreshold;
});
if (potentialLeaks.length > 0) {
leaksDetected += potentialLeaks.length;
console.warn(`[LEAK-DETECTION] Se detectaron ${potentialLeaks.length} posibles fugas de conexiones para empresa ${empresaId}`);
// Si las conexiones son muy antiguas (más de 5 minutos), cerrarlas forzosamente
const forceCloseThreshold = 5 * 60 * 1000; // 5 minutos
const toForceClose = potentialLeaks.filter(conn => {
const timestamp = conn.__cuentiCreatedAt;
const lastActivity = conn.__cuentiLastActivity || timestamp;
const timeSinceActivity = now.getTime() - ((lastActivity === null || lastActivity === void 0 ? void 0 : lastActivity.getTime()) || timestamp.getTime());
return timeSinceActivity > forceCloseThreshold;
});
if (toForceClose.length > 0) {
console.warn(`[LEAK-DETECTION] Cerrando forzosamente ${toForceClose.length} conexiones muy antiguas (>5min) para empresa ${empresaId}`);
for (const conn of toForceClose) {
try {
const threadId = conn.threadId || 'desconocido';
const createdTime = conn.__cuentiCreatedAt;
const createdAgo = createdTime ? `${Math.round((now.getTime() - createdTime.getTime()) / 1000)}s` : 'tiempo desconocido';
yield this.disconnectConnection(conn);
totalClosed++;
console.log(`[LEAK-DETECTION] Conexión forzosamente cerrada: thread=${threadId}, empresa=${empresaId}, creada hace=${createdAgo}`);
}
catch (error) {
console.error(`[LEAK-DETECTION] Error al cerrar conexión con fuga:`, error);
}
}
}
}
// Continuar con la limpieza normal de conexiones antiguas
// Cerrar conexiones antiguas (pool base)
if (empresaId === 0 && set.size > 3) {
const toClose = Array.from(set)
.filter(conn => {
const timestamp = conn.__cuentiCreatedAt;
if (timestamp && (now.getTime() - timestamp.getTime() > 5 * 60 * 1000)) {
return true;
}
return false;
})
.slice(0, set.size - 2); // Dejar al menos 2 conexiones
for (const conn of toClose) {
yield this.disconnectConnection(conn);
totalClosed++;
}
}
// Para otras empresas
else if (set.size > 5) {
const toClose = Array.from(set)
.filter(conn => {
const timestamp = conn.__cuentiCreatedAt;
if (timestamp && (now.getTime() - timestamp.getTime() > 10 * 60 * 1000)) {
return true;
}
return false;
})
.slice(0, set.size - 3); // Dejar al menos 3 conexiones
for (const conn of toClose) {
yield this.disconnectConnection(conn);
totalClosed++;
}
}
}
return { closed: totalClosed, leaksDetected };
});
}
// Obtener estadísticas detalladas del estado de conexiones
getConnectionStats() {
var _a, _b;
const stats = {
total: {
connections: 0,
pools: 0,
servers: this.listaServer.length
},
byEmpresa: {},
pools: [],
isShuttingDown: DBManager.isShuttingDown()
};
// Estadísticas de conexiones por empresa
const now = new Date();
for (const [empresaId, connections] of this.connections.entries()) {
stats.total.connections += connections.size;
// Analizar conexiones para esta empresa
const activeThreshold = 60 * 1000; // 1 minuto
const leakThreshold = 2 * 60 * 1000; // 2 minutos
let active = 0;
let idle = 0;
let potentialLeaks = 0;
for (const conn of connections) {
const createdAt = conn.__cuentiCreatedAt;
const lastActivity = conn.__cuentiLastActivity || createdAt;
if (!createdAt) {
active++; // Si no tiene timestamp, asumir activa
continue;
}
const timeSinceActivity = now.getTime() - ((lastActivity === null || lastActivity === void 0 ? void 0 : lastActivity.getTime()) || createdAt.getTime());
if (timeSinceActivity < activeThreshold) {
active++;
}
else if (timeSinceActivity >= leakThreshold) {
potentialLeaks++;
}
else {
idle++;
}
}
stats.byEmpresa[empresaId] = {
total: connections.size,
active,
idle,
potential_leaks: potentialLeaks
};
}
// Estadísticas de pools de servidores
for (const server of this.listaServer) {
if (server.pool) {
stats.total.pools++;
try {
// Intentar obtener estadísticas del pool
const poolStats = ((_b = (_a = server.pool).getStats) === null || _b === void 0 ? void 0 : _b.call(_a)) || {};
stats.pools.push({
id_servidor: server.id_servidor,
ip: server.ip,
pool_stats: poolStats,
empresas: server.lstEmpresas.length,
lastHealthCheck: server.lastHealthCheck
});
}
catch (error) {
stats.pools.push({
id_servidor: server.id_servidor,
error: 'No se pudieron obtener estadísticas del pool'
});
}
}
}
return stats;
}
cleanup() {
return __awaiter(this, void 0, void 0, function* () {
try {
console.log("[SHUTDOWN] Iniciando proceso de limpieza completa...");
// Detener timer de limpieza
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
console.log("[SHUTDOWN] Timer de limpieza detenido");
}
// Cierre seguro y ordenado de las conexiones
const errors = [];
// 1. Primero intentar cerrar todas las conexiones actualmente rastreadas
console.log("[SHUTDOWN] Cerrando conexiones activas...");
const connectionPromises = [];
let totalConnections = 0;
for (const [empresaId, connections] of this.connections.entries()) {
totalConnections += connections.size;
for (const conn of connections) {
connectionPromises.push(this.disconnectConnection(conn).catch(err => {
console.error(`[SHUTDOWN] Error al cerrar conexión de empresa ${empresaId}:`, err.message);
errors.push(err);
}));
}
}
if (connectionPromises.length > 0) {
// Esperar con un timeout para evitar bloqueos
yield Promise.race([
Promise.all(connectionPromises),
new Promise(resolve => setTimeout(resolve, 3000))
]);
console.log(`[SHUTDOWN] Se intentó cerrar ${totalConnections} conexiones activas`);
}
// 2. Después cerrar los pools de manera ordenada
console.log("[SHUTDOWN] Cerrando pools de servidores...");
for (const server of this.listaServer) {
if (server.pool) {
try {
yield Promise.race([
server.pool.end(),
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout closing tenant pool")), 2000))
]);
console.log(`[SHUTDOWN] Pool del servidor ${server.id_servidor} cerrado correctamente`);
}
catch (error) {
console.error(`[SHUTDOWN] Error cerrando pool del servidor ${server.id_servidor}:`, error);
errors.push(error);
}
finally {
server.pool = null;
}
}
}
// 3. Finalmente cerrar el pool principal
console.log("[SHUTDOWN] Cerrando pool principal...");
if (this.pool_bases) {
try {
yield Promise.race([
this.pool_bases.end(),
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout closing main pool")), 2000))
]);
console.log("[SHUTDOWN] Pool principal cerrado correctamente");
}
catch (error) {
console.error("[SHUTDOWN] Error cerrando pool principal:", error);
errors.push(error);
}
finally {
this.pool_bases = null;
}
}
// Limpiar referencias
this.isInitialized = false;
this.listaServer = [];
this.connections.clear();
// Desreferenciar listeners para evitar múltiples manejadores
process.removeAllListeners("SIGINT");
process.removeAllListeners("SIGTERM");
process.removeAllListeners("SIGQUIT");
process.removeAllListeners("uncaughtException");
process.removeAllListeners("unhandledRejection");
console.log("[SHUTDOWN] Proceso de limpieza completado");
if (errors.length > 0) {
console.warn(`[SHUTDOWN] Se encontraron ${errors.length} errores durante el cierre, pero el proceso continuó`);
}
}
catch (error) {
console.error("[SHUTDOWN] Error fatal durante cleanup:", error);
}
});
}
// Método para desconectar todas las conexiones de una empresa específica
disconnectEmpresa(idEmpresa) {
return __awaiter(this, void 0, void 0, function* () {
let disconnected = 0;
const connections = this.connections.get(idEmpresa);
if (!connections || connections.size === 0) {
return 0;
}
// Crear una copia del conjunto para evitar problemas durante la iteración
const connectionsToClose = Array.from(connections);
for (const conn of connectionsToClose) {
try {
yield this.disconnectConnection(conn);
disconnected++;
}
catch (error) {
console.error(`[DISCONNECT] Error al desconectar conexión para empresa ${idEmpresa}:`, error);
}
}
return disconnected;
});
}
}
DBManager.shutdownInProgress = false;
// Funciones exportadas para API pública
export const disconnectConnection = (conn) => __awaiter(void 0, void 0, void 0, function* () {
const manager = DBManager.getInstance();
return manager.disconnectConnection(conn);
});
/**
* Desconecta todas las conexiones asociadas a una empresa específica
* @param {number} idEmpresa - ID de la empresa cuyas conexiones serán desconectadas
* @returns {Promise<number>} Número de conexiones desconectadas
*/
export const disconnectEmpresa = (idEmpresa) => __awaiter(void 0, void 0, void 0, function* () {
const manager = DBManager.getInstance();
if (typeof manager.disconnectEmpresa === 'function') {
return manager.disconnectEmpresa(idEmpresa);
}
return 0;
});
/**
* Comprueba si el sistema está en proceso de cierre
* @returns {boolean} true si el sistema está cerrándose
*/
export const isShuttingDown = DBManager.isShuttingDown;
/**
* Realiza un cierre seguro de todas las conexiones de base de datos
* Usa este método antes de terminar la aplicación para evitar conexiones zombies
* @returns {Promise<void>} Promesa que se resuelve cuando se completa el cierre
*/
export const shutdown = () => __awaiter(void 0, void 0, void 0, function* () {
// Si ya está en proceso de cierre, simplemente devolver una promesa resuelta
if (DBManager.isShuttingDown()) {
console.log("[SHUTDOWN] Ya se está ejecutando el cierre, ignorando llamada adicional a shutdown()");
return;
}
console.log("[SHUTDOWN] Iniciando cierre seguro mediante llamada a shutdown()...");
try {
// Establecer la bandera de cierre
DBManager.setShuttingDown(true);
// Obtener la instancia del gestor y forzar la limpieza
const manager = DBManager.getInstance();
// Acceder al método cleanup privado mediante una función wrapper
const cleanup = () => __awaiter(void 0, void 0, void 0, function* () {
if (typeof manager.cleanup === 'function') {
yield manager.cleanup();
}
else {
console.warn("[SHUTDOWN] Método cleanup no encontrado, intentando closeAll");
// Fallback por si cambia la implementación
if (typeof manager.closeAll === 'function') {
yield manager.closeAll();
}
}
});
// Ejecutar la limpieza
yield cleanup();
console.log("[SHUTDOWN] Cierre completo mediante llamada a shutdown()");
}
catch (error) {
console.error("[SHUTDOWN] Error durante shutdown():", error);
throw error; // Re-lanzar para que el llamador pueda manejarlo
}
});
/**
* Limpia las conexiones antiguas que podrían estar consumiendo recursos
* Además, detecta y maneja posibles fugas de conexiones
* @returns {Promise<{closed: number, leaksDetected: number}>} Número de conexiones cerradas y fugas detectadas
*/
export const cleanupStaleConnections = () => __awaiter(void 0, void 0, void 0, function* () {
const manager = DBManager.getInstance();
if (typeof manager.cleanupStaleConnections === 'function') {
return manager.cleanupStaleConnections();
}
return { closed: 0, leaksDetected: 0 };
});
/**
* Obtiene estadísticas detalladas sobre el estado de las conexiones
* Útil para diagnosticar problemas de fugas de conexiones (connection leaks)
* @returns {Record<string, any>} Estadísticas detalladas de las conexiones
*/
export const getConnectionStats = () => {
const manager = DBManager.getInstance();
if (typeof manager.getConnectionStats === 'function') {
return manager.getConnectionStats();
}
return { error: 'Función no disponible' };
};
export { DBManager };
export default DBManager.getInstance();