UNPKG

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
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();