- VERSION 5.09.14

-feat: Implementación robusta de monitoreo de pool de conexiones y peticiones activas

-Este commit resuelve problemas críticos en el monitoreo del pool de conexiones (C3P0) y el conteo de peticiones activas por base de datos, mejorando significativamente la visibilidad y fiabilidad del rendimiento del servidor jRDC2-Multi.

-Problemas Identificados y Resueltos:

-1.  **Métricas de `BusyConnections` y `TotalConnections` inconsistentes o siempre en `0` en el `Manager` y `query_logs`:**
		    *   **Problema**: Anteriormente, la métrica `busy_connections` en `query_logs` a menudo reportaba `0` o no reflejaba el estado real. De manera similar, el panel de `Manager?command=totalcon` consistentemente mostraba `BusyConnections: 0` y `TotalConnections` estancadas en `InitialPoolSize`, a pesar de que Oracle sí reportaba conexiones activas. Esto generaba confusión sobre el uso real y la expansión del pool.
		    *   **Solución**: Se modificó la lógica en los *handlers* (`DBHandlerJSON.bas` y `DBHandlerB4X.bas`) para capturar la métrica `BusyConnections` directamente del pool de C3P0 **inmediatamente después de que el *handler* adquiere una conexión** (`con = Connector.GetConnection(finalDbKey)`). Este valor se pasa explícitamente a la subrutina `Main.LogQueryPerformance` para su registro en `query_logs` y para ser consumido por `Manager.bas` a través de `RDCConnector.GetPoolStats`. Esto garantiza que el valor registrado y reportado refleje con precisión el número de conexiones activas en el instante de su adquisición. Pruebas exhaustivas confirmaron que C3P0 sí reporta conexiones ocupadas y sí expande `TotalConnections` hasta `MaxPoolSize` cuando la demanda lo exige.

-2.  **Contador `handler_active_requests` no decrementaba correctamente:**
		    *   **Problema**: El contador de peticiones activas por base de datos (`GlobalParameters.ActiveRequestsCountByDB`) no mostraba un decremento consistente, resultando en un conteo que solo aumentaba o mostraba valores erráticos en los logs.
		    *   **Solución**:
		        *   Se aseguró la declaración `Public ActiveRequestsCountByDB As Map` en `GlobalParameters.bas`.
		        *   Se garantizó su inicialización como un `srvr.CreateThreadSafeMap` en `Main.AppStart` para un manejo concurrente seguro de los contadores.
		        *   En `DBHandlerJSON.bas`, la `dbKey` (obtenida del parámetro `dbx` del JSON) ahora se resuelve *antes* de incrementar el contador, asegurando que el incremento y el decremento se apliquen siempre a la misma clave de base de datos correcta.
		        *   Se implementó una coerción explícita a `Int` (`.As(Int)`) para todas las operaciones de lectura y escritura (`GetDefault`, `Put`) en `GlobalParameters.ActiveRequestsCountByDB`, resolviendo problemas de tipo que causaban inconsistencias y el fallo en el decremento.
		        *   La lógica de decremento en `Private Sub CleanupAndLog` (presente en ambos *handlers*) se hizo más robusta, verificando que el contador sea mayor que cero antes de decrementar para evitar valores negativos.

-Beneficios de estos Cambios:

		*   **Monitoreo Preciso y Fiable**: Las métricas `busy_connections` y `handler_active_requests` en `query_logs` y el panel `Manager` ahora son totalmente fiables, proporcionando una visión clara y en tiempo real del uso del pool de conexiones y la carga de peticiones activas por base de datos.
		*   **Diagnóstico Mejorado**: La visibilidad interna del estado del pool de C3P0 durante las pruebas confirma que la configuración de `RDCConnector` es correcta y que el pool se expande y contrae según lo esperado por la demanda.
		*   **Robustez del Código**: La gestión de contadores de peticiones activas es ahora consistente, thread-safe y a prueba de fallos de tipo, mejorando la estabilidad general del servidor bajo carga.
This commit is contained in:
2025-09-17 01:53:07 -06:00
parent e04cdded47
commit 2ec8f5973f
10 changed files with 479 additions and 263 deletions

View File

@@ -53,7 +53,7 @@ Version=10.3
#Region Project Attributes
#CommandLineArgs:
#MergeLibraries: True
' VERSION 5.09.014
' VERSION 5.09.14
'###########################################################################################################
'###################### PULL #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull
@@ -66,18 +66,6 @@ Version=10.3
'###########################################################################################################
#End Region
'- VERSION 5.09.08
'- Se agregó que se puedan configurar en el config.properties los siguientes parametros:
'
' - setInitialPoolSize = 3
' - setMinPoolSize = 2
' - setMaxPoolSize = 5
'
'- Se agregaron en duro a RDConnector los siguientes parametros:
'
' - setMaxIdleTime <-- Tiempo máximo de inactividad de la conexión.
' - setMaxConnectionAge <-- Tiempo de vida máximo de una conexión.
' - setCheckoutTimeout <-- Tiempo máximo de espera por una conexión.
'change based on the jdbc jar file
'#AdditionalJar: mysql-connector-java-5.1.27-bin
'#AdditionalJar: postgresql-42.7.0
@@ -105,6 +93,7 @@ Sub Process_Globals
Public SQL1 As SQL ' Objeto SQL para interactuar con la base de datos de usuarios (SQLite).
Private bc As BCrypt ' Objeto para realizar operaciones de hashing de contraseñas de forma segura (para autenticación).
Public MainConnectorsLock As JavaObject ' Objeto de bloqueo para proteger Main.Connectors
Public ActiveRequestsCountByDB As Map
End Sub
Sub AppStart (Args() As String)
@@ -120,6 +109,8 @@ Sub AppStart (Args() As String)
GlobalParameters.mpTotalRequests.Initialize ' Mapa para contar peticiones por endpoint/DB.
GlobalParameters.mpTotalConnections.Initialize ' Mapa para almacenar el estado de los pools de conexión por DB.
GlobalParameters.mpBlockConnection.Initialize ' Mapa para gestionar IPs bloqueadas (si la funcionalidad está activa).
GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap ' Aseguramos que sea thread-safe para conteo de peticiones activas por DB [___new 3.txt, conversación]
' 3. Inicializa las estructuras principales del servidor HTTP.
listaDeCP.Initialize ' Inicializa la lista que contendrá los IDs de las bases de datos.
@@ -145,105 +136,182 @@ Sub AppStart (Args() As String)
' en el mismo directorio donde se ejecuta el JAR.
cpFiles = File.ListFiles("./")
If cpFiles.Size > 0 Then
For i = 0 To cpFiles.Size - 1
' Procesa 'config.DB2.properties'
If cpFiles.Get(i) = "config.DB2.properties" Then
Dim con2 As RDCConnector ' Declara una variable específica y única para el conector de DB2.
con2.Initialize("DB2") ' Inicializa la instancia del conector para "DB2".
Connectors.Put("DB2", con2) ' Asocia "DB2" con su instancia de RDCConnector.
listaDeCP.Add("DB2") ' Añade "DB2" a la lista de bases de datos.
Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.")
End If
For i = 0 To cpFiles.Size - 1
' Procesa 'config.DB2.properties'
If cpFiles.Get(i) = "config.DB2.properties" Then
Dim con2 As RDCConnector ' Declara una variable específica y única para el conector de DB2.
con2.Initialize("DB2") ' Inicializa la instancia del conector para "DB2".
Connectors.Put("DB2", con2) ' Asocia "DB2" con su instancia de RDCConnector.
listaDeCP.Add("DB2") ' Añade "DB2" a la lista de bases de datos.
Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.")
End If
' Procesa 'config.DB3.properties'
If cpFiles.Get(i) = "config.DB3.properties" Then
Dim con3 As RDCConnector ' Declara una variable específica y única para el conector de DB3.
con3.Initialize("DB3") ' Inicializa la instancia del conector para "DB3".
Connectors.Put("DB3", con3) ' Asocia "DB3" con su instancia de RDCConnector.
listaDeCP.Add("DB3") ' Añade "DB3" a la lista de bases de datos.
Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.")
End If
' Procesa 'config.DB3.properties'
If cpFiles.Get(i) = "config.DB3.properties" Then
Dim con3 As RDCConnector ' Declara una variable específica y única para el conector de DB3.
con3.Initialize("DB3") ' Inicializa la instancia del conector para "DB3".
Connectors.Put("DB3", con3) ' Asocia "DB3" con su instancia de RDCConnector.
listaDeCP.Add("DB3") ' Añade "DB3" a la lista de bases de datos.
Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.")
End If
' Procesa 'config.DB4.properties'
If cpFiles.Get(i) = "config.DB4.properties" Then
Dim con4 As RDCConnector ' Declara una variable específica y única para el conector de DB4.
con4.Initialize("DB4") ' Inicializa la instancia del conector para "DB4".
Connectors.Put("DB4", con4) ' Asocia "DB4" con su instancia de RDCConnector.
listaDeCP.Add("DB4") ' Añade "DB4" a la lista de bases de datos.
Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.")
End If
Next
Else
Log("Main.AppStart: No se encontraron archivos de configuración adicionales (config.DBx.properties).")
End If
' Procesa 'config.DB4.properties'
If cpFiles.Get(i) = "config.DB4.properties" Then
Dim con4 As RDCConnector ' Declara una variable específica y única para el conector de DB4.
con4.Initialize("DB4") ' Inicializa la instancia del conector para "DB4".
Connectors.Put("DB4", con4) ' Asocia "DB4" con su instancia de RDCConnector.
listaDeCP.Add("DB4") ' Añade "DB4" a la lista de bases de datos.
Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.")
End If
Next
Else
Log("Main.AppStart: No se encontraron archivos de configuración adicionales (config.DBx.properties).")
End If
' Log final de las bases de datos que el servidor está gestionando.
Dim sbListaDeCP_Log As StringBuilder
sbListaDeCP_Log.Initialize
For Each item As String In listaDeCP
sbListaDeCP_Log.Append(item).Append(", ")
Next
If sbListaDeCP_Log.Length > 0 Then
sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length) ' Elimina la última ", "
End If
Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$)
' Log final de las bases de datos que el servidor está gestionando.
Dim sbListaDeCP_Log As StringBuilder
sbListaDeCP_Log.Initialize
For Each item As String In listaDeCP
sbListaDeCP_Log.Append(item).Append(", ")
Next
If sbListaDeCP_Log.Length > 0 Then
sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length) ' Elimina la última ", "
End If
Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$)
' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR ===
' Asocia rutas URL específicas con clases que manejarán las peticiones correspondientes.
' El último parámetro (True) indica que el handler se ejecutará en un nuevo hilo,
' lo que es recomendable para la mayoría de los casos para evitar bloqueos.
srvr.AddHandler("/ping", "ping", True) ' Endpoint simple para verificar si el servidor está activo.
srvr.AddHandler("/test", "TestHandler", True) ' Endpoint para pruebas de conexión y estado del servidor.
srvr.AddHandler("/login", "LoginHandler", True) ' Muestra la página HTML de login.
srvr.AddHandler("/dologin", "DoLoginHandler", True) ' Procesa el intento de inicio de sesión.
srvr.AddHandler("/logout", "LogoutHandler", True) ' Cierra la sesión del usuario.
srvr.AddHandler("/changepass", "ChangePassHandler", True) ' Permite a los usuarios cambiar su contraseña.
srvr.AddHandler("/manager", "Manager", True) ' Panel de administración del servidor (requiere autenticación).
srvr.AddHandler("/DBJ", "DBHandlerJSON", True) ' Handler para clientes web (ej. JavaScript, Node.js) que usan JSON.
srvr.AddHandler("/dbrquery", "DBHandlerJSON", True) ' Un alias para el handler JSON, por si se usa en clientes específicos.
srvr.AddHandler("/favicon.ico", "faviconHandler", True) ' Sirve el icono de la página (favicon).
srvr.AddHandler("/*", "DBHandlerB4X", True) ' Handler por defecto para clientes B4X (DBRequestManager),
' procesa peticiones dinámicamente según la URL.
' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR ===
' Asocia rutas URL específicas con clases que manejarán las peticiones correspondientes.
' El último parámetro (True) indica que el handler se ejecutará en un nuevo hilo,
' lo que es recomendable para la mayoría de los casos para evitar bloqueos.
srvr.AddHandler("/ping", "ping", True) ' Endpoint simple para verificar si el servidor está activo.
srvr.AddHandler("/test", "TestHandler", True) ' Endpoint para pruebas de conexión y estado del servidor.
srvr.AddHandler("/login", "LoginHandler", True) ' Muestra la página HTML de login.
srvr.AddHandler("/dologin", "DoLoginHandler", True) ' Procesa el intento de inicio de sesión.
srvr.AddHandler("/logout", "LogoutHandler", True) ' Cierra la sesión del usuario.
srvr.AddHandler("/changepass", "ChangePassHandler", True) ' Permite a los usuarios cambiar su contraseña.
srvr.AddHandler("/manager", "Manager", True) ' Panel de administración del servidor (requiere autenticación).
srvr.AddHandler("/DBJ", "DBHandlerJSON", False) ' Handler para clientes web (ej. JavaScript, Node.js) que usan JSON.
srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) ' Un alias para el handler JSON, por si se usa en clientes específicos.
srvr.AddHandler("/favicon.ico", "faviconHandler", True) ' Sirve el icono de la página (favicon).
srvr.AddHandler("/*", "DBHandlerB4X", False) ' Handler por defecto para clientes B4X (DBRequestManager),
' procesa peticiones dinámicamente según la URL.
' 7. Inicia el servidor HTTP.
srvr.Start
Log("===========================================================")
Log($"-=== jRDC está funcionando en el puerto: ${srvr.Port} (versión = $1.2{VERSION}) ===-"$)
Log("===========================================================")
' 8. Inicia el bucle de mensajes de B4J. Es esencial para que la aplicación
' de servidor continúe ejecutándose y procesando eventos.
StartMessageLoop
' 7. Inicia el servidor HTTP.
srvr.Start
Log("===========================================================")
Log($"-=== jRDC está funcionando en el puerto: ${srvr.Port} (versión = $1.2{VERSION}) ===-"$)
Log("===========================================================")
' 8. Inicia el bucle de mensajes de B4J. Es esencial para que la aplicación
' de servidor continúe ejecutándose y procesando eventos.
StartMessageLoop
End Sub
' --- Subrutina para inicializar la base de datos de usuarios local (SQLite) ---
' Esta base de datos se utiliza para almacenar credenciales de usuarios que pueden
' acceder al panel de administración del servidor jRDC.
' acceder al panel de administración del servidor jRDC y los logs de queries.
Sub InitializeSQLiteDatabase
Dim dbFileName As String = "users.db" ' Nombre del archivo de la base de datos SQLite.
' Verifica si el archivo de la base de datos ya existe en el directorio de la aplicación.
If File.Exists(File.DirApp, dbFileName) = False Then
Log("Creando nueva base de datos de usuarios: " & dbFileName)
' Inicializa la conexión a la base de datos SQLite, creándola si no existe (último parámetro en True).
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
Dim dbFileName As String = "users.db" ' Nombre del archivo de la base de datos SQLite.
' Verifica si el archivo de la base de datos ya existe en el directorio de la aplicación.
If File.Exists(File.DirApp, dbFileName) = False Then
Log("Creando nueva base de datos de usuarios: " & dbFileName)
' Inicializa la conexión a la base de datos SQLite, creándola si no existe (último parámetro en True).
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
' Define y ejecuta la sentencia SQL para crear la tabla 'users'.
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
SQL1.ExecNonQuery(createUserTable)
' >>> INICIO: Creación de la tabla query_logs con las nuevas columnas desde CERO <<<
Log("Creando tabla 'query_logs' con columnas de rendimiento.")
Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)"
SQL1.ExecNonQuery(createQueryLogsTable)
' >>> FIN: Creación de la tabla query_logs <<<
' Crea un usuario por defecto para facilitar el primer acceso al panel de administración.
Dim defaultUser As String = "admin"
Dim defaultPass As String = "12345"
' Genera un hash seguro de la contraseña usando BCrypt, lo cual es crucial para la seguridad.
Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt)
' Inserta el usuario por defecto en la tabla 'users'.
SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass))
Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$)
Else
' Si el archivo de la base de datos ya existe, simplemente se abre.
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
Log("Base de datos de usuarios cargada.")
' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<<
Log("Verificando y migrando tabla 'query_logs' si es necesario.")
' Define y ejecuta la sentencia SQL para crear la tabla 'users'.
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
SQL1.ExecNonQuery(createUserTable)
' Crea un usuario por defecto para facilitar el primer acceso al panel de administración.
Dim defaultUser As String = "admin"
Dim defaultPass As String = "12345"
' Genera un hash seguro de la contraseña usando BCrypt, lo cual es crucial para la seguridad.
Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt)
' Inserta el usuario por defecto en la tabla 'users'.
SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass))
Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$)
Else
' Si el archivo de la base de datos ya existe, simplemente se abre.
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
Log("Base de datos de usuarios cargada.")
End If
' Primero, verificar si la tabla query_logs existe.
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then
Log("Tabla 'query_logs' no encontrada, creándola con columnas de rendimiento.")
Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)"
SQL1.ExecNonQuery(createQueryLogsTable)
Else
' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes.
Dim columnExists As Boolean
Dim rs As ResultSet
' --- VERIFICAR Y AÑADIR busy_connections ---
columnExists = False
' Ejecutamos PRAGMA sin WHERE y lo filtramos en código.
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then
columnExists = True
Exit ' La columna ya existe, salimos del bucle.
End If
Loop
rs.Close ' ¡Importante cerrar el ResultSet!
If columnExists = False Then
Log("Añadiendo columna 'busy_connections' a query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0")
End If
' --- VERIFICAR Y AÑADIR handler_active_requests ---
columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)") ' Ejecutamos PRAGMA nuevamente para esta columna.
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("handler_active_requests") Then
columnExists = True
Exit ' La columna ya existe, salimos del bucle.
End If
Loop
rs.Close ' ¡Importante cerrar el ResultSet!
If columnExists = False Then
Log("Añadiendo columna 'handler_active_requests' a query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0")
End If
End If
' >>> FIN: Lógica de migración (ALTER TABLE) <<<
End If
End Sub
' Subrutina para registrar las métricas de rendimiento de las queries
Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int)
Try
' El valor PoolBusyConnections ya se recibe directamente del handler.
' Removemos la lógica anterior de obtenerlo del conector.
' Dim connector As RDCConnector = Main.Connectors.Get(DbKey).As(RDCConnector)
' Dim poolBusyConnections As Int = 0
' If connector.IsInitialized Then
' Dim poolStats As Map = connector.GetPoolStats
' If poolStats.ContainsKey("BusyConnections") Then
' poolBusyConnections = poolStats.Get("BusyConnections")
' End If
' Else
' Log($"ADVERTENCIA: Conector RDC para ${DbKey} no inicializado al intentar loguear rendimiento."$)
' End If
' Insertamos los datos en la tabla query_logs de SQLite
SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests) VALUES (?, ?, ?, ?, ?, ?, ?)", _
Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests))
Catch
Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message)
End Try
End Sub