- VERSION 5.09.15

1.  **Nuevas Funcionalidades en el Panel de Administración (Manager):**
	*   Se añadió el comando `slowqueries` al `Manager` para permitir la visualización de las 20 consultas más lentas registradas en la tabla `query_logs` de SQLite [22].
	*   Se mejoró el comando `totalcon` en `Manager.bas` para mostrar estadísticas detalladas de *todos* los pools de conexión C3P0 configurados, obteniendo métricas en tiempo real (TotalConnections, BusyConnections, IdleConnections, etc.) de cada `RDCConnector` [2, 22].
	*   Beneficio: Mayor visibilidad y control proactivo sobre el rendimiento y el uso de recursos del servidor desde la interfaz de administración.

	2.  **Optimización de la Gestión de Logs (`query_logs`):**
	*   Se implementó un `Public timerLogs As Timer` en `Main.bas` [conversación], que se inicializa en `AppStart` y ejecuta periódicamente (cada 10 minutos) la subrutina `borraArribaDe15000Logs`.
	*   La subrutina `borraArribaDe15000Logs` recorta la tabla `query_logs` en `users.db` para mantener solo los 15,000 registros más recientes, y luego realiza un `vacuum` para optimizar el espacio en disco utilizado por la base de datos SQLite [conversación].
	*   Beneficio: Prevención del crecimiento excesivo de la base de datos de logs de rendimiento, manteniendo un historial manejable y optimizando el uso del almacenamiento a largo plazo.
This commit is contained in:
2025-09-17 11:37:59 -06:00
parent 2ec8f5973f
commit 51c829b876
8 changed files with 905 additions and 699 deletions

View File

@@ -53,7 +53,7 @@ Version=10.3
#Region Project Attributes
#CommandLineArgs:
#MergeLibraries: True
' VERSION 5.09.14
' VERSION 5.09.15
'###########################################################################################################
'###################### PULL #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull
@@ -75,42 +75,68 @@ Version=10.3
Sub Process_Globals
' --- Variables globales accesibles desde cualquier parte del proyecto ---
Public srvr As Server ' El objeto principal del servidor HTTP de B4J.
Public const VERSION As Float = 2.23 ' La versión actual de este servidor jRDC modificado.
' Tipos personalizados para la serialización y deserialización de datos
' Objeto principal del servidor HTTP de B4J.
Public srvr As Server
' La versión actual de este servidor jRDC modificado.
Public const VERSION As Float = 2.23
' Tipos personalizados (clases) para la serialización y deserialización de datos
' entre el cliente B4X (DBRequestManager) y el servidor jRDC2.
Type DBCommand (Name As String, Parameters() As Object) ' Define un comando SQL.
Type DBResult (Tag As Object, Columns As Map, Rows As List) ' Define la estructura de un resultado de consulta.
Public listaDeCP As List ' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2").
Private cpFiles As List ' Una lista temporal para almacenar los nombres de archivos encontrados en el directorio.
' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2").
Public listaDeCP As List
' Una lista temporal para almacenar los nombres de archivos de configuración encontrados.
Private cpFiles As List
' Mapas globales para gestionar los conectores de base de datos y los comandos SQL.
Public Connectors, commandsMap As Map ' Connectors: Almacena las instancias de RDCConnector por DB.
' commandsMap: Almacena los comandos SQL cargados para cada DB.
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
' Connectors: Almacena las instancias de RDCConnector por cada base de datos (DB1, DB2, etc.).
' commandsMap: Almacena los comandos SQL cargados de los archivos de configuración para cada DB.
Public Connectors, commandsMap As Map
' Objeto SQL para interactuar con la base de datos de usuarios y logs (SQLite).
Public SQL1 As SQL
' Objeto para realizar operaciones de hashing de contraseñas de forma segura (para autenticación de Manager).
Private bc As BCrypt
' Objeto de bloqueo (ReentrantLock) para proteger el mapa Main.Connectors durante operaciones de recarga (Hot-Swap).
Public MainConnectorsLock As JavaObject
' Mapa para contar las peticiones activas por cada base de datos. Es thread-safe.
Public ActiveRequestsCountByDB As Map
' Timer para ejecutar tareas periódicas, como la limpieza de logs.
Public timerLogs As Timer
End Sub
Sub AppStart (Args() As String)
' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
bc.Initialize("BC")
' 1. Inicializa la base de datos local de usuarios (SQLite).
' Esta base de datos se crea automáticamente si no existe y contiene los usuarios para el panel de administración.
' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs.
' Esta base de datos se crea automáticamente si no existe o se migra si es necesario.
InitializeSQLiteDatabase
' <<<< Bloque de inicialización del Timer para la limpieza de logs >>>>
' Inicializa y configura el Timer para borrar logs antiguos cada 10 minutos (600,000 milisegundos).
timerLogs.Initialize("TimerLogs", 600000) ' 10 minutos = 600 * 1000 = 600000 ms
timerLogs.Enabled = True ' Habilita el timer para que empiece a correr.
Log("Main.AppStart: Timer de limpieza de 'query_logs' inicializado para ejecutarse cada 10 minutos.")
' <<<< Fin del bloque del Timer >>>>
' 2. Inicializa los mapas globales definidos en GlobalParameters.bas.
' Estos mapas se usan para monitorear el servidor y gestionar configuraciones dinámicas.
GlobalParameters.mpLogs.Initialize ' Mapa para almacenar logs de actividad.
GlobalParameters.mpLogs.Initialize ' Mapa para almacenar logs de actividad general.
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]
' Aseguramos que el mapa de conteo de peticiones activas sea thread-safe para un manejo concurrente seguro.
GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap
' 3. Inicializa las estructuras principales del servidor HTTP.
listaDeCP.Initialize ' Inicializa la lista que contendrá los IDs de las bases de datos.
@@ -118,9 +144,8 @@ Sub AppStart (Args() As String)
Connectors = srvr.CreateThreadSafeMap ' Crea un mapa seguro para almacenar instancias de RDCConnector (un conector por DB).
commandsMap.Initialize ' Inicializa el mapa que almacenará los comandos SQL cargados de los archivos de configuración.
' <<<< NUEVA INICIALIZACIÓN: Creamos una instancia de ReentrantLock para proteger Main.Connectors >>>>
' Creamos una instancia de ReentrantLock para proteger Main.Connectors durante operaciones atómicas de Hot-Swap.
MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null)
' <<<< HASTA AQUÍ LA NUEVA INICIALIZACIÓN >>>>
' === 4. INICIALIZACIÓN DEL CONECTOR PARA LA BASE DE DATOS PRINCIPAL (DB1) ===
' DB1 siempre usa el archivo 'config.properties' por defecto.
@@ -128,7 +153,7 @@ Sub AppStart (Args() As String)
con1.Initialize("DB1") ' Inicializa la instancia del conector para "DB1".
Connectors.Put("DB1", con1) ' Asocia el identificador "DB1" con su instancia de RDCConnector.
srvr.Port = con1.serverPort ' El puerto del servidor HTTP se obtiene del config.properties de DB1.
listaDeCP.Add("DB1") ' Añade "DB1" a la lista de bases de datos gestionadas.]
listaDeCP.Add("DB1") ' Añade "DB1" a la lista de bases de datos gestionadas.
Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$)
' === 5. DETECCIÓN E INICIALIZACIÓN DE BASES DE DATOS ADICIONALES (DB2, DB3, DB4) ===
@@ -145,7 +170,6 @@ Sub AppStart (Args() As String)
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.
@@ -154,7 +178,6 @@ Sub AppStart (Args() As String)
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.
@@ -183,16 +206,16 @@ Sub AppStart (Args() As String)
' 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("/ping", "ping", False) ' Endpoint simple para verificar si el servidor está activo.
srvr.AddHandler("/test", "TestHandler", False) ' Endpoint para pruebas de conexión y estado del servidor.
srvr.AddHandler("/login", "LoginHandler", False) ' Muestra la página HTML de login.
srvr.AddHandler("/dologin", "DoLoginHandler", False) ' Procesa el intento de inicio de sesión.
srvr.AddHandler("/logout", "LogoutHandler", False) ' Cierra la sesión del usuario.
srvr.AddHandler("/changepass", "ChangePassHandler", False) ' Permite a los usuarios cambiar su contraseña.
srvr.AddHandler("/manager", "Manager", False) ' 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("/favicon.ico", "faviconHandler", False) ' 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.
@@ -219,12 +242,13 @@ Sub InitializeSQLiteDatabase
' 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'.
' Define y ejecuta la sentencia SQL para crear la tabla 'users' para la autenticación del Manager.
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.")
' Esta tabla almacena métricas detalladas de cada query ejecutada.
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 <<<
@@ -244,20 +268,20 @@ Sub InitializeSQLiteDatabase
' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<<
Log("Verificando y migrando tabla 'query_logs' si es necesario.")
' 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.
' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes (busy_connections, handler_active_requests).
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.
' Ejecutamos PRAGMA para obtener la información de la tabla y verificar la existencia de la columna.
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then
@@ -265,7 +289,7 @@ Sub InitializeSQLiteDatabase
Exit ' La columna ya existe, salimos del bucle.
End If
Loop
rs.Close ' ¡Importante cerrar el ResultSet!
rs.Close ' ¡Importante cerrar el ResultSet para liberar recursos!
If columnExists = False Then
Log("Añadiendo columna 'busy_connections' a query_logs.")
@@ -292,26 +316,34 @@ Sub InitializeSQLiteDatabase
End If
End Sub
' Subrutina para registrar las métricas de rendimiento de las queries
' Subrutina para registrar las métricas de rendimiento de las queries en la tabla 'query_logs'.
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
' Los valores de PoolBusyConnections y HandlerActiveRequests ya se reciben directamente del handler,
' eliminando la necesidad de obtenerlos del conector en este punto.
' 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
End Sub
' Subrutina de evento para el Timer 'timerLogs'.
' Se ejecuta periódicamente (cada 10 minutos) para limpiar la tabla de logs.
Sub TimerLogs_Tick
Try
borraArribaDe15000Logs ' Llama a la función para limpiar los logs.
Catch
Log("ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message)
End Try
End Sub
' Borra los registros más antiguos de la tabla 'query_logs', manteniendo solo los 15,000 más recientes.
' Luego, optimiza el espacio de la base de datos SQLite con un 'vacuum'.
Sub borraArribaDe15000Logs 'ignore
Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.")
SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )")
SQL1.ExecNonQuery("vacuum;") ' Optimiza el espacio de almacenamiento de la base de datos.
End Sub