Files
jRDC-Multi/RDCConnector.bas
Jose Alberto Guerra Ugalde 8b876e5095 - VERSION 5.09.19
- feat(sqlite): Implementa optimización de SQLite (WAL e Índices)
- fix(manager): Extiende el comando 'test' para verificar todos los pools de conexión configurados.

- Mejoras al subsistema de logs y diagnóstico del servidor jRDC2-Multi.
- Cambios principales:
1. Optimización del Rendimiento de SQLite (users.db):
*   Habilitación de WAL: Se implementó PRAGMA journal_mode=WAL y PRAGMA synchronous=NORMAL en `InitializeSQLiteDatabase`. Esto reduce la contención de disco y mejora el rendimiento de I/O en las escrituras transaccionales de logs por lotes.
*   Índices de logs: Se agregaron índices a las columnas `timestamp` y `duration_ms` en `query_logs`, y a `timestamp` en `errores`. Esto acelera drásticamente las operaciones de limpieza periódica (`borraArribaDe15000Logs`) y la generación de reportes de consultas lentas (`slowqueries`).

2. Mejora del Comando de Diagnóstico 'test':
*   Se corrigió el comando `manager?command=test` para que no solo pruebe la conexión de `DB1`, sino que itere sobre `Main.listaDeCP` y fuerce la adquisición y liberación de una conexión (`GetConnection`) en *todos* los `RDCConnector` configurados (DB1, DB2, DB3, etc.).
*   La nueva lógica garantiza una prueba de vida rigurosa de cada pool C3P0, devolviendo un mensaje detallado del estado de conectividad y registrando un error crítico vía `LogServerError` si algún pool no responde.
2025-09-27 20:34:12 -06:00

351 lines
18 KiB
QBasic

B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=4.19
@EndOfDesignText@
' Módulo de clase: RDCConnector
' Esta clase gestiona el pool de conexiones a una base de datos específica utilizando la librería C3P0.
' Cada instancia de RDCConnector es responsable de una base de datos definida en un archivo 'config.DBx.properties'.
' Se encarga de inicializar el pool, obtener conexiones, cargar comandos SQL y proporcionar estadísticas del pool.
Sub Class_Globals
' --- Variables globales de la clase ---
' Objeto principal para gestionar el pool de conexiones de la base de datos (usa C3P0 internamente).
Private pool As ConnectionPool
' Bandera para activar/desactivar el modo de depuración de queries.
' Cuando está en True, los comandos SQL se recargan en cada petición (útil en desarrollo).
' Private DebugQueries As Boolean
' Almacena los comandos SQL específicos de esta base de datos, cargados de su archivo de configuración.
Public commands As Map
' El puerto que el servidor HTTP usará. Este valor se lee del 'config.properties' de la base de datos principal (DB1).
Public serverPort As Int
' Indica si se debe usar el pool de conexiones. Siempre True en este diseño, ya que C3P0 es esencial.
Public usePool As Boolean = True
' Almacena la configuración completa (DriverClass, JdbcUrl, User, Password, InitialPoolSize, etc.)
' cargada de su respectivo archivo .properties.
Public config As Map
' Indica si la tolerancia a parámetros de más está activa.
Public IsParameterToleranceEnabled As Boolean
End Sub
' Subrutina de inicialización para el conector de una base de datos específica.
' Se llama una vez por cada base de datos (DB1, DB2, DB3, DB4) al iniciar el servidor en Main.AppStart.
' DB: El identificador único de la base de datos (ej. "DB1", "DB2").
Public Sub Initialize(DB As String)
' Si el identificador es "DB1", se usa una cadena vacía para que File.ReadMap cargue "config.properties" (el archivo por defecto).
If DB.EqualsIgnoreCase("DB1") Then DB = ""
' PASO 1: Cargar la configuración desde el archivo .properties correspondiente.
' Es CRUCIAL que se asigne a la variable de CLASE 'config' (sin 'Dim' local)
' para que la configuración cargada del archivo sea persistente para esta instancia del conector.
config = LoadConfigMap(DB)
' Leer la configuración de tolerancia de parámetros
Dim toleranceSetting As Int = config.GetDefault("parameterTolerance", 0).As(Int) ' Por defecto, 0 (estricto)
IsParameterToleranceEnabled = (toleranceSetting = 1) ' La tolerancia se habilita si el valor es 1
If IsParameterToleranceEnabled Then
Log($"RDCConnector.Initialize para ${DB}: Tolerancia a parámetros de más, HABILITADA."$)
Else
Log($"RDCConnector.Initialize para ${DB}: Tolerancia a parámetros de más, DESHABILITADA (modo estricto)."$)
End If
' Bloque Try-Catch para la inicialización y configuración del pool.
' Esto es esencial para capturar cualquier error crítico que impida la conexión inicial a la base de datos.
Try
' PASO 2: Inicializar el objeto B4X ConnectionPool.
' Esto crea la instancia subyacente de com.mchange.v2.c3p0.ComboPooledDataSource (la librería C3P0).
' Se le pasan los parámetros básicos para que C3P0 pueda construirse.
pool.Initialize(config.Get("DriverClass"), config.Get("JdbcUrl"), config.Get("User"), config.Get("Password"))
' Obtener la referencia JavaObject para acceder a métodos de configuración avanzados de C3P0.
Dim jo As JavaObject = pool
' PASO 3: Aplicar *todas* las propiedades de configuración de C3P0 INMEDIATAMENTE.
' Esto debe ocurrir *después* de 'pool.Initialize' pero *antes* de que C3P0 intente realmente adquirir conexiones.
' Esto asegura que las configuraciones sean efectivas desde el primer intento de conexión.
' Lectura de los valores desde el archivo de configuración, con valores por defecto si no se encuentran.
Dim initialPoolSize As Int = config.GetDefault("InitialPoolSize", 3)
Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2)
Dim maxPoolSize As Int = config.GetDefault("MaxPoolSize", 5)
Dim acquireIncrement As Int = config.GetDefault("AcquireIncrement", 5)
Dim maxIdleTime As Int = config.GetDefault("MaxIdleTime", 300)
Dim maxConnectionAge As Int = config.GetDefault("MaxConnectionAge", 900)
Dim checkoutTimeout As Int = config.GetDefault("CheckoutTimeout", 60000)
' Configuración de los parámetros del pool de conexiones C3P0:
jo.RunMethod("setInitialPoolSize", Array(initialPoolSize)) ' Define el número de conexiones que se intentarán crear al iniciar el pool.
jo.RunMethod("setMinPoolSize", Array(minPoolSize)) ' Fija el número mínimo de conexiones que el pool mantendrá abiertas.
jo.RunMethod("setMaxPoolSize", Array(maxPoolSize)) ' Define el número máximo de conexiones simultáneas.
jo.RunMethod("setAcquireIncrement", Array(acquireIncrement)) ' Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
jo.RunMethod("setMaxIdleTime", Array As Object(maxIdleTime)) ' Es el tiempo máximo (en segundos) que una conexión puede permanecer inactiva en el pool antes de ser cerrada para ahorrar recursos.
jo.RunMethod("setMaxConnectionAge", Array As Object(maxConnectionAge)) ' Tiempo máximo de vida de una conexión (segundos), previene conexiones viciadas.
jo.RunMethod("setCheckoutTimeout", Array As Object(checkoutTimeout)) ' Tiempo máximo de espera por una conexión del pool (milisegundos).
' LÍNEAS CRÍTICAS PARA FORZAR UN COMPORTAMIENTO NO SILENCIOSO DE C3P0:
' Por defecto, C3P0 puede reintentar muchas veces y no lanzar una excepción si las conexiones iniciales fallan.
' Estas líneas fuerzan a C3P0 a ser estricto y reportar errores de inmediato.
jo.RunMethod("setAcquireRetryAttempts", Array As Object(2)) ' Limita los intentos iniciales de adquisición a 1.
jo.RunMethod("setBreakAfterAcquireFailure", Array As Object(True)) ' ¡Forza a C3P0 a lanzar una excepción si falla al adquirir conexiones!
' PASO 4: Forzar la creación de conexiones iniciales y verificar el estado.
' Este paso es VITAL. Obliga a C3P0 a intentar establecer las conexiones iniciales (InitialPoolSize)
' *con la configuración ya establecida*. Si hay un problema de conectividad real, la excepción
' se capturará aquí y se reportará, evitando "fallos silenciosos".
Dim tempCon As SQL = pool.GetConnection ' Adquiere una conexión para forzar al pool a inicializarse.
If tempCon.IsInitialized Then
tempCon.Close ' Devolvemos la conexión inmediatamente al pool para que esté disponible.
End If
' Cargar configuración estática en el cache global
Dim dbKeyToStore As String = DB
If dbKeyToStore = "" Then dbKeyToStore = "DB1" ' Aseguramos la llave si era DB1
Dim initialPoolStats As Map = GetPoolStats ' Llama a la función que usa JavaObject
' PASO C: Almacenamos el mapa completo (estático + dinámico inicial) en el cache global.
Main.LatestPoolStats.Put(dbKeyToStore, initialPoolStats)
' Log(Main.LatestPoolStats)
' com.mchange.v2.c3p0.ComboPooledDataSource [
' acquireIncrement -> 3,
' acquireRetryAttempts -> 30,
' acquireRetryDelay -> 1000,
' autoCommitOnClose -> False,
' automaticTestTable -> Null,
' breakAfterAcquireFailure -> False,
' checkoutTimeout -> 20000,
' connectionCustomizerClassName -> Null,
' connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester,
' contextClassLoaderSource -> caller,
' dataSourceName -> 2rvxvdb7cyxd8zlw6dyb|63021689,
' debugUnreturnedConnectionStackTraces -> False,
' description -> Null,
' driverClass -> oracle.jdbc.driver.OracleDriver,
' extensions -> {},
' factoryClassLocation -> Null,
' forceIgnoreUnresolvedTransactions -> False,
' forceSynchronousCheckins -> False,
' forceUseNamedDriverClass -> False,
' identityToken -> 2rvxvdb7cyxd8zlw6dyb|63021689,
' idleConnectionTestPeriod -> 600,
' initialPoolSize -> 3,
' jdbcUrl -> jdbc:oracle:thin:@//10.0.0.110:1521/DBKMT,
' maxAdministrativeTaskTime -> 0,
' maxConnectionAge -> 0,
' maxIdleTime -> 1800,
' maxIdleTimeExcessConnections -> 0,
' maxPoolSize -> 5,
' maxStatements -> 150,
' maxStatementsPerConnection -> 0,
' minPoolSize -> 3,
' numHelperThreads -> 3,
' preferredTestQuery -> DBMS_SESSION.SET_IDENTIFIER('whatever'),
' privilegeSpawnedThreads -> False,
' properties -> {password=******, user=******},
' propertyCycle -> 0,
' statementCacheNumDeferredCloseThreads -> 0,
' testConnectionOnCheckin -> False,
' testConnectionOnCheckout -> True,
' unreturnedConnectionTimeout -> 0,
' userOverrides -> {},
' usesTraditionalReflectiveProxies -> False
' ]
'
Catch
' Si ocurre un error durante la inicialización del pool o al forzar la conexión,
' este Log es CRÍTICO para el diagnóstico, especialmente en un entorno de producción.
Dim ErrorMsg As String = $"RDCConnector.Initialize para ${DB}: ERROR CRÍTICO al inicializar/forzar conexión: ${LastException.Message}"$
Log(ErrorMsg)
Main.LogServerError("ERROR", "RDCConnector.Initialize", ErrorMsg, DB, Null, Null)
End Try
' Configuración de depuración de queries. Se activa automáticamente si el proyecto se ejecuta en modo DEBUG.
' #If DEBUG
' DebugQueries = True ' Descomentar para activar la recarga de comandos en cada petición en desarrollo.
' #Else
' DebugQueries = False
' #End If
' Se obtiene el puerto del servidor HTTP desde la configuración de esta base de datos.
' Nota: En el diseño actual, el puerto principal lo define DB1 (config.properties).
serverPort = config.Get("ServerPort")
' Asegura que el identificador DB no sea una cadena vacía para la carga de comandos.
' Esto es relevante si DB era "DB1" y se convirtió a "" al inicio de esta subrutina.
If DB = "" Then DB = "DB1"
' Carga los comandos SQL predefinidos de esta base de datos en el mapa global 'commandsMap' de Main.
LoadSQLCommands(config, DB)
End Sub
' Carga el mapa de configuración (JdbcUrl, User, Password, etc.) desde el archivo .properties correspondiente.
' DB: El identificador de la base de datos (ej. "DB1", "DB2").
' Retorna un Mapa con la configuración leída.
Private Sub LoadConfigMap(DB As String) As Map
Private DBX As String = ""
If DB <> "" Then DBX = "." & DB ' Construye el sufijo del nombre de archivo (ej. ".DB2").
Log($"RDCConnector.LoadConfigMap: Leemos el config${DBX}.properties"$) ' Mantenemos este log para confirmación de carga.
Return File.ReadMap("./", "config" & DBX & ".properties")
End Sub
' Obtiene la sentencia SQL completa para un comando dado desde el mapa de comandos cargado.
' DB: El identificador de la base de datos.
' Key: El nombre del comando SQL (ej. "select_user").
' Retorna la sentencia SQL como String.
Public Sub GetCommand(DB As String, Key As String) As String
' Obtiene los comandos de la DB específica del mapa global Main.commandsMap.
commands = Main.commandsMap.Get(DB).As(Map)
If commands.ContainsKey("sql." & Key) = False Then
Dim ErrorMsg As String = $"RDCConnector.GetCommand: *** Comando no encontrado: '${Key}' para DB: '${DB}' ***"$
Log(ErrorMsg)
Main.LogServerError("ERROR", "RDCConnector.GetCommand", ErrorMsg, DB, Key, Null) ' Log importante si un comando no se encuentra.
End If
Return commands.Get("sql." & Key) ' Retorna la sentencia SQL.
End Sub
' Obtiene una conexión SQL funcional del pool de conexiones para la base de datos especificada.
' DB: El identificador de la base de datos.
' Retorna un objeto SQL (la conexión a la base de datos).
Public Sub GetConnection(DB As String) As SQL
If DB.EqualsIgnoreCase("DB1") Then DB = ""
' En modo de depuración, recarga los comandos SQL en cada petición.
' Esto permite modificar queries en config.properties sin reiniciar el servidor durante el desarrollo.
' If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB)
' <<<< Bloque de Logs de Depuración de Adquisición de Conexión (descomentar si es necesario) >>>>
' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Solicitando conexión del pool..."$)
Dim conn As SQL = pool.GetConnection
' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Conexión obtenida. IsInitialized: ${conn.IsInitialized}"$)
If pool.IsInitialized Then ' Doble verificación del estado del pool para logging más seguro
' Dim jo As JavaObject = pool
' Aseguramos que los valores de C3P0 sean Ints, manejando posibles retornos como Double.
' Dim busyCount As Int = jo.RunMethod("getNumBusyConnectionsAllUsers", Null).As(Object).As(Int)
' Dim totalCount As Int = jo.RunMethod("getNumConnectionsAllUsers", Null).As(Object).As(Int)
' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Estadísticas del Pool (después de obtener): Busy=${busyCount}, Total=${totalCount}"$)
End If
' <<<< Fin del bloque de Logs de Depuración >>>>
Return conn ' Retorna una conexión del pool.
End Sub
' Carga todos los comandos SQL del mapa de configuración en el mapa global 'commandsMap' de Main.
' config2: El mapa de configuración de la DB actual (JdbcUrl, User, Password, etc.).
' DB: El identificador de la base de datos.
Private Sub LoadSQLCommands(config2 As Map, DB As String)
Dim newCommands As Map
newCommands.Initialize
For Each k As String In config2.Keys
If k.StartsWith("sql.") Then ' Busca claves que comiencen con "sql." (ej. "sql.select_user").
newCommands.Put(k, config2.Get(k)) ' Añade el comando al mapa.
End If
Next
commands = newCommands ' Actualiza el mapa de comandos de esta instancia de RDCConnector.
Main.commandsMap.Put(DB, commands) ' Almacena el mapa de comandos en el mapa global 'commandsMap' de Main.
End Sub
' Nuevo: Obtiene estadísticas detalladas del pool de conexiones.
' Es utilizado por el Manager para mostrar el estado del pool.
Public Sub GetPoolStats As Map
Dim stats As Map
stats.Initialize
' Log("--- RDCConnector.GetPoolStats llamado ---") ' Log de inicio (descomentar si es necesario)
If pool.IsInitialized Then
' Log("RDCConnector.GetPoolStats: Pool está inicializado. Intentando obtener métricas.") ' Log (descomentar si es necesario)
Dim jo As JavaObject = pool ' Convertimos el objeto pool a JavaObject para acceder a sus métodos internos de C3P0.
Try
' --- Métricas en tiempo real del pool ---
' Se obtienen los valores y se aseguran como objetos para su posterior manejo en el mapa.
Dim totalConn As Object = jo.RunMethod("getNumConnectionsAllUsers", Null)
stats.Put("TotalConnections", totalConn)
' Log($"RDCConnector.GetPoolStats: TotalConnections = ${totalConn}"$) ' Log (descomentar si es necesario)
Dim busyConn As Object = jo.RunMethod("getNumBusyConnectionsAllUsers", Null)
stats.Put("BusyConnections", busyConn)
' Log($"RDCConnector.GetPoolStats: BusyConnections = ${busyConn}"$) ' Log (descomentar si es necesario)
Dim idleConn As Object = jo.RunMethod("getNumIdleConnectionsAllUsers", Null)
stats.Put("IdleConnections", idleConn)
' Log($"RDCConnector.GetPoolStats: IdleConnections = ${idleConn}"$) ' Log (descomentar si es necesario)
' --- Valores de configuración del pool (para referencia) ---
' Se obtienen y almacenan los parámetros de configuración del pool.
Dim initialSize As Object = jo.RunMethod("getInitialPoolSize", Null)
stats.Put("InitialPoolSize", initialSize)
' Log($"RDCConnector.GetPoolStats: InitialPoolSize = ${initialSize}"$) ' Log (descomentar si es necesario)
Dim minSize As Object = jo.RunMethod("getMinPoolSize", Null)
stats.Put("MinPoolSize", minSize)
' Log($"RDCConnector.GetPoolStats: MinPoolSize = ${minSize}"$) ' Log (descomentar si es necesario)
Dim maxSize As Object = jo.RunMethod("getMaxPoolSize", Null)
stats.Put("MaxPoolSize", maxSize)
' Log($"RDCConnector.GetPoolStats: MaxPoolSize = ${maxSize}"$) ' Log (descomentar si es necesario)
Dim acquireInc As Object = jo.RunMethod("getAcquireIncrement", Null)
stats.Put("AcquireIncrement", acquireInc)
' Log($"RDCConnector.GetPoolStats: AcquireIncrement = ${acquireInc}"$) ' Log (descomentar si es necesario)
Dim maxIdle As Object = jo.RunMethod("getMaxIdleTime", Null)
stats.Put("MaxIdleTime", maxIdle)
' Log($"RDCConnector.GetPoolStats: MaxIdleTime = ${maxIdle}"$) ' Log (descomentar si es necesario)
Dim maxAge As Object = jo.RunMethod("getMaxConnectionAge", Null)
stats.Put("MaxConnectionAge", maxAge)
' Log($"RDCConnector.GetPoolStats: MaxConnectionAge = ${maxAge}"$) ' Log (descomentar si es necesario)
Dim checkoutTime As Object = jo.RunMethod("getCheckoutTimeout", Null)
stats.Put("CheckoutTimeout", checkoutTime)
' Log($"RDCConnector.GetPoolStats: CheckoutTimeout = ${checkoutTime}"$) ' Log (descomentar si es necesario)
Catch
' Si ocurre un error al obtener las estadísticas, se registra y se añade un mensaje de error al mapa.
Dim ErrorMsg As String = "RDCConnector.GetPoolStats: ERROR CRÍTICO al obtener estadísticas del pool: " & LastException.Message
Log(ErrorMsg)
Main.LogServerError("ERROR", "RDCConnector.GetPoolStats", ErrorMsg, "Todas", Null, Null) ' <-- Nuevo Log
stats.Put("Error", LastException.Message)
End Try
Else
' Si el pool no está inicializado, se registra una advertencia y se devuelve un mapa con un error.
Dim WarningMsg As String = "RDCConnector.GetPoolStats: ADVERTENCIA: Pool NO está inicializado. Retornando mapa con error."
Log(WarningMsg)
Main.LogServerError("ADVERTENCIA", "RDCConnector.GetPoolStats", WarningMsg, "Todas", Null, Null) ' <-- Nuevo Log
stats.Put("Error", "Pool de conexiones no inicializado para esta DB.")
End If
' Se utiliza JSONGenerator para serializar el mapa de estadísticas a String para el log,
' lo que permite una visualización estructurada y fácil de leer.
Dim tempJsonGen As JSONGenerator
tempJsonGen.Initialize(stats)
' Log("--- RDCConnector.GetPoolStats finalizado. Retornando stats: " & tempJsonGen.ToString & " ---") ' Log de fin (descomentar si es necesario)
Return stats
End Sub
' *** NUEVA SUBRUTINA: Cierra el pool de conexiones de forma ordenada usando JavaObject ***
' Este método es crucial para liberar los recursos de la base de datos cuando un conector RDC
' ya no es necesario o va a ser reemplazado (por ejemplo, durante un "Hot-Swap" de configuración).
Public Sub Close
If pool <> Null And pool.IsInitialized Then
' Log($"RDCConnector.Close: Cerrando pool de conexiones."$) ' Log (descomentar si es necesario)
' Convertimos el objeto pool de B4X a un JavaObject para poder llamar a su método 'close()'
' que no está expuesto directamente en la envoltura de B4X, asegurando un cierre limpio de C3P0.
Dim joPool As JavaObject = pool
joPool.RunMethod("close", Null) ' Llamamos al método 'close()' del objeto Java subyacente de C3P0.
End If
End Sub