diff --git a/Cambios.bas b/Cambios.bas index c6ef7a6..6c3213f 100644 --- a/Cambios.bas +++ b/Cambios.bas @@ -16,6 +16,34 @@ Sub Process_Globals '- Agregar una forma de probar con carga el servidor '- Agregar la opcion de "Queries lentos" + ' 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. + + + '- VERSION 5.09.13.3 '- Implementación de "Hot-Swap" para recarga de configuraciones de DB sin reiniciar el servidor. '- Migración a ReentrantLock para sincronización debido a incompatibilidad con 'Sync'. diff --git a/ChangePassHandler.bas b/ChangePassHandler.bas index de14585..3be2adb 100644 --- a/ChangePassHandler.bas +++ b/ChangePassHandler.bas @@ -35,7 +35,7 @@ Public Sub Handle(req As ServletRequest, resp As ServletResponse) Log("--- Probando con contraseña fija ---") Log("Valor de la BD (storedHash): " & storedHash) - If storedHash = Null Or bc.checkpw("12345", storedHash) = False Then ' <<--- CAMBIO CLAVE AQUÍ + If storedHash = Null Or bc.checkpw(currentPass, storedHash) = False Then ' <<--- CAMBIO CLAVE AQUÍ resp.Write("") Return End If diff --git a/DBHandlerB4X.bas b/DBHandlerB4X.bas index 5c20cf0..ad897ab 100644 --- a/DBHandlerB4X.bas +++ b/DBHandlerB4X.bas @@ -35,100 +35,136 @@ End Sub ' Método principal que maneja cada petición HTTP que llega a este servlet. Sub Handle(req As ServletRequest, resp As ServletResponse) - ' === INICIO DE LA LÓGICA DINÁMICA === - ' Extrae la URI completa de la petición (ej. /DB1/endpoint). + ' === INICIO DE LA LÓGICA DINÁMICA (Extracción de dbKey de la URL) === Dim URI As String = req.RequestURI - ' Variable para almacenar la "llave" o identificador de la base de datos (ej. "DB1"). - Dim dbKey As String - - ' Comprueba si la URI tiene contenido y empieza con "/". + Dim dbKey As String ' Usamos dbKey para consistencia con tu código original. If URI.Length > 1 And URI.StartsWith("/") Then - ' Extrae la parte de la URI que viene después del primer "/". - dbKey = URI.Substring(1) - ' Si la llave contiene más "/", se queda solo con la primera parte. - ' Esto permite URLs como /DB1/clientes o /DB2/productos, extrayendo "DB1" o "DB2". + dbKey = URI.Substring(1) '[DBHandlerB4X.bas.txt, 51] If dbKey.Contains("/") Then - dbKey = dbKey.SubString2(0, dbKey.IndexOf("/")) + dbKey = dbKey.SubString2(0, dbKey.IndexOf("/")) '[DBHandlerB4X.bas.txt, 51] End If Else - ' Si la URI está vacía o es "/", usa "DB1" como la base de datos por defecto. - dbKey = "DB1" + dbKey = "DB1" '[DBHandlerB4X.bas.txt, 51] End If + dbKey = dbKey.ToUpperCase '[DBHandlerB4X.bas.txt, 52] - ' Convierte la llave a mayúsculas para que no sea sensible a mayúsculas/minúsculas (ej. "db1" se convierte en "DB1"). - dbKey = dbKey.ToUpperCase - - ' Verifica si la llave de la base de datos extraída existe en la configuración de conectores. - If Main.Connectors.ContainsKey(dbKey) = False Then - ' Si no existe, crea un mensaje de error claro. - Dim ErrorMsg As String = $"Invalid DB key specified in URL: '${dbKey}'. Valid keys are: ${Main.listaDeCP}"$ - ' Registra el error en el log del servidor. - Log(ErrorMsg) - ' Envía una respuesta de error 400 (Bad Request) al cliente en formato de texto plano. - SendPlainTextError(resp, 400, ErrorMsg) - ' Termina la ejecución de este método. + If Main.Connectors.ContainsKey(dbKey) = False Then '[DBHandlerB4X.bas.txt, 52] + Dim ErrorMsg As String = $"Invalid DB key specified in URL: '${dbKey}'. Valid keys are: ${Main.listaDeCP}"$ '[DBHandlerB4X.bas.txt, 52] + Log(ErrorMsg) '[DBHandlerB4X.bas.txt, 52] + SendPlainTextError(resp, 400, ErrorMsg) '[DBHandlerB4X.bas.txt, 52] + ' Aquí no se necesita CleanupAndLog, ya que el contador no se ha incrementado + ' y no se ha obtenido ninguna conexión del pool aún. Return End If ' === FIN DE LA LÓGICA DINÁMICA === - Log("********************* " & dbKey & " ********************") - ' Guarda el tiempo de inicio para medir la duración de la petición. - Dim start As Long = DateTime.Now - ' Variable para almacenar el nombre del comando SQL a ejecutar. - Dim q As String - ' Obtiene el stream de entrada de la petición, que contiene los datos enviados por el cliente. - Dim in As InputStream = req.InputStream - ' Obtiene el parámetro "method" de la URL (ej. ?method=query2). - Dim method As String = req.GetParameter("method") - ' Obtiene el conector correspondiente a la base de datos seleccionada. - Connector = Main.Connectors.Get(dbKey) - ' Declara la variable para la conexión a la base de datos. - Dim con As SQL - Try - ' Obtiene una conexión del pool de conexiones. - con = Connector.GetConnection(dbKey) - Log("Metodo: " & method) - ' Determina qué función ejecutar basándose en el parámetro "method". + Log("********************* " & dbKey & " ********************") '[DBHandlerB4X.bas.txt, 53] + + Dim start As Long = DateTime.Now '[___new 3.txt, 203] + + ' --- INICIO: Conteo de peticiones activas para esta dbKey (Incrementar) --- + Dim currentActiveRequests As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0) '[___new 3.txt, 205] + GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentActiveRequests + 1) '[___new 3.txt, 205] + Dim requestsBeforeDecrement As Int = currentActiveRequests + 1 '[___new 3.txt, 207] + ' --- FIN: Conteo de peticiones activas --- + + ' Declaraciones de variables con alcance en toda la subrutina para la limpieza. + Dim q As String = "unknown_b4x_command" ' Nombre del comando para el log, con valor por defecto. + Dim con As SQL ' La conexión a la BD, se inicializará más tarde. + Dim duration As Long ' La duración de la petición, calculada antes del log. + Dim poolBusyConnectionsForLog As Int = 0 ' Contiene el número de conexiones ocupadas del pool. + + Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler --- + Dim in As InputStream = req.InputStream '[DBHandlerB4X.bas.txt, 53] + Dim method As String = req.GetParameter("method") '[DBHandlerB4X.bas.txt, 53] + Connector = Main.Connectors.Get(dbKey) '[DBHandlerB4X.bas.txt, 54] + + con = Connector.GetConnection(dbKey) ' La conexión a la BD se obtiene aquí. [DBHandlerB4X.bas.txt, 54] + + ' <<<< ¡BUSY_CONNECTIONS YA SE CAPTURABA BIEN! >>>> + If Connector.IsInitialized Then + Dim poolStats As Map = Connector.GetPoolStats '[___new 3.txt, 204] + If poolStats.ContainsKey("BusyConnections") Then + poolBusyConnectionsForLog = poolStats.Get("BusyConnections") ' Capturamos el valor. + End If + End If + ' <<<< ¡FIN DE CAPTURA! >>>> + + Log("Metodo: " & method) '[DBHandlerB4X.bas.txt, 54] + If method = "query2" Then - ' Ejecuta una consulta usando el protocolo más nuevo (B4XSerializator). - q = ExecuteQuery2(dbKey, con, in, resp) + q = ExecuteQuery2(dbKey, con, in, resp) '[DBHandlerB4X.bas.txt, 54] + If q = "error" Then ' Si ExecuteQuery2 devolvió un error de validación. + duration = DateTime.Now - start + CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return + End If '#if VERSION1 Else if method = "query" Then - ' Protocolo antiguo: descomprime el stream y ejecuta la consulta. in = cs.WrapInputStream(in, "gzip") - q = ExecuteQuery(dbKey, con, in, resp) + q = ExecuteQuery(dbKey, con, in, resp) '[DBHandlerB4X.bas.txt, 55] + If q = "error" Then + duration = DateTime.Now - start + CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return + End If Else if method = "batch" Then - ' Protocolo antiguo: descomprime el stream y ejecuta un lote de comandos. in = cs.WrapInputStream(in, "gzip") - q = ExecuteBatch(dbKey, con, in, resp) + q = ExecuteBatch(dbKey, con, in, resp) '[DBHandlerB4X.bas.txt, 55] + If q = "error" Then + duration = DateTime.Now - start + CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return + End If '#end if Else if method = "batch2" Then - ' Ejecuta un lote de comandos usando el protocolo más nuevo. - q = ExecuteBatch2(dbKey, con, in, resp) + q = ExecuteBatch2(dbKey, con, in, resp) '[DBHandlerB4X.bas.txt, 55] + If q = "error" Then + duration = DateTime.Now - start + CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return + End If Else - ' Si el método es desconocido, lo registra y envía un error. - Log("Unknown method: " & method) - SendPlainTextError(resp, 500, "unknown method") + Log("Unknown method: " & method) '[DBHandlerB4X.bas.txt, 56] + SendPlainTextError(resp, 500, "unknown method") '[DBHandlerB4X.bas.txt, 56] + q = "unknown_method_handler" ' Aseguramos un valor para q en el log. + duration = DateTime.Now - start + CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return End If - Catch - ' Si ocurre cualquier error en el bloque Try, lo captura. - Log(LastException) - ' Envía un error 500 (Internal Server Error) al cliente con el mensaje de la excepción. - SendPlainTextError(resp, 500, LastException.Message) - End Try - ' Asegura que la conexión a la BD se cierre y se devuelva al pool. - If con <> Null And con.IsInitialized Then con.Close - ' Registra en el log el comando ejecutado, cuánto tiempo tardó y la IP del cliente. - Log($"Command: ${q}, took: ${DateTime.Now - start}ms, client=${req.RemoteAddress}"$) - ' *** NUEVO: Insertar el log en la base de datos SQLite *** - Dim duration As Long = DateTime.Now - start - Try - Main.SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip) VALUES (?, ?, ?, ?, ?)", _ - Array As Object(q, duration, DateTime.Now, dbKey, req.RemoteAddress)) - Catch - Log("Error al guardar log de query en SQLite (DBHandlerB4X): " & LastException.Message) - End Try + Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL --- + Log(LastException) '[DBHandlerB4X.bas.txt, 56] + SendPlainTextError(resp, 500, LastException.Message) '[DBHandlerB4X.bas.txt, 56] + q = "error_in_b4x_handler" ' Aseguramos un valor para q en el log si hay excepción. + End Try ' --- FIN: Bloque Try principal --- + + ' --- Lógica de logging y limpieza final (para rutas de ejecución normal o después de Catch) --- + duration = DateTime.Now - start '[DBHandlerB4X.bas.txt, 57] + Log($"Command: ${q}, took: ${duration}ms, client=${req.RemoteAddress}"$) '[DBHandlerB4X.bas.txt, 57] + CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + +End Sub + +' --- NUEVA SUBRUTINA: Centraliza el logging y la limpieza --- +Private Sub CleanupAndLog(dbKey As String, qName As String, durMs As Long, clientIp As String, handlerReqs As Int, poolBusyConns As Int, conn As SQL) + ' 1. Llama a la subrutina centralizada para registrar el rendimiento. + Main.LogQueryPerformance(qName, durMs, dbKey, clientIp, handlerReqs, poolBusyConns) '[___new 3.txt, 207] + + ' <<<< ¡CORRECCIÓN CLAVE AQUÍ! >>>> + ' 2. Decrementa el contador de peticiones activas para esta dbKey de forma más robusta. + Dim currentCount As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0) + If currentCount > 0 Then + GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1) + Else + ' Si el contador ya está en 0 o negativo, registramos una advertencia y lo aseguramos en 0. + Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$) + GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0) + End If + ' <<<< ¡FIN DE CORRECCIÓN CLAVE! >>>> + + ' 3. Asegura que la conexión a la BD siempre se cierre y se devuelva al pool. + If conn <> Null And conn.IsInitialized Then conn.Close End Sub ' Ejecuta una consulta única usando el protocolo V2 (B4XSerializator). diff --git a/DBHandlerJSON.bas b/DBHandlerJSON.bas index 01edce3..f16f28a 100644 --- a/DBHandlerJSON.bas +++ b/DBHandlerJSON.bas @@ -18,125 +18,132 @@ End Sub ' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp). ' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp). Sub Handle(req As ServletRequest, resp As ServletResponse) - ' --- Headers CORS (Cross-Origin Resource Sharing) --- - ' Estos encabezados son necesarios para permitir que un cliente web (ej. una página con JavaScript) - ' que se encuentra en un dominio diferente pueda hacer peticiones a este servidor. - resp.SetHeader("Access-Control-Allow-Origin", "*") ' Permite peticiones desde cualquier origen. - resp.SetHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") ' Métodos HTTP permitidos. - resp.SetHeader("Access-Control-Allow-Headers", "Content-Type") ' Encabezados permitidos en la petición. + resp.SetHeader("Access-Control-Allow-Origin", "*") + resp.SetHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + resp.SetHeader("Access-Control-Allow-Headers", "Content-Type") - ' El método OPTIONS es una "petición de comprobación previa" (preflight request) que envían los navegadores - ' para verificar los permisos CORS antes de enviar la petición real (ej. POST). - ' Si es una petición OPTIONS, simplemente terminamos la ejecución sin procesar nada más. - If req.Method = "OPTIONS" Then Return + If req.Method = "OPTIONS" Then + Return ' Las peticiones OPTIONS no incrementan contadores ni usan BD, así que salimos directamente. + End If - ' Establece "DB1" como el nombre de la base de datos por defecto. - Dim DB As String = "DB1" + Dim start As Long = DateTime.Now - ' Obtiene el objeto conector para la base de datos por defecto o especificada. - Connector = Main.Connectors.Get(DB) + ' Declaraciones de variables con alcance en toda la subrutina para la limpieza. + Dim con As SQL ' La conexión a la BD, se inicializará más tarde. + Dim queryNameForLog As String = "unknown_json_command" ' Nombre del comando para el log, con valor por defecto. + Dim duration As Long ' La duración de la petición, calculada antes del log. + Dim poolBusyConnectionsForLog As Int = 0 ' Contiene el número de conexiones ocupadas del pool. + + Dim finalDbKey As String = "DB1" + Dim requestsBeforeDecrement As Int = 0 ' Se inicializa en 0. - ' Declara una variable para la conexión SQL. - Dim con As SQL + Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler --- - ' Inicia un bloque Try...Catch para manejar posibles errores durante la ejecución. - Try Dim jsonString As String - - ' *** INICIO DE LA LÓGICA CORREGIDA PARA LEER JSON DEL CUERPO (POST) O DE PARÁMETROS (GET/POST Form-urlencoded) *** If req.Method = "POST" And req.ContentType.Contains("application/json") Then - ' Para peticiones POST con Content-Type: application/json, el JSON viene en el cuerpo (InputStream). - Dim Is0 As InputStream = req.InputStream ' ¡CORREGIDO: Usamos Is0 en lugar de Is para evitar conflicto con palabra reservada! - ' Usamos Bit.InputStreamToBytes de la librería jcore para leer el stream completo a un array de bytes. + Dim Is0 As InputStream = req.InputStream Dim bytes() As Byte = Bit.InputStreamToBytes(Is0) - jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") ' Convertimos los bytes a String UTF8. - Is0.Close ' ¡Es CRÍTICO cerrar el InputStream para liberar los recursos del sistema! + jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") + Is0.Close Else - ' Para peticiones GET o POST con Content-Type como 'application/x-www-form-urlencoded', - ' el JSON se espera en el parámetro 'j' de la URL. jsonString = req.GetParameter("j") End If - ' *** FIN DE LA LÓGICA CORREGIDA PARA LEER JSON *** - ' Verifica si la cadena JSON obtenida es nula o está vacía. If jsonString = Null Or jsonString = "" Then - ' Si falta el JSON, envía una respuesta de error 400 (Bad Request) y termina la ejecución. SendErrorResponse(resp, 400, "Falta el parámetro 'j' en el URL o el cuerpo JSON en la petición.") - Return + duration = DateTime.Now - start + CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return ' Salida temprana. End If - ' Crea un objeto JSONParser para analizar la cadena JSON. Dim parser As JSONParser parser.Initialize(jsonString) - - ' Convierte la cadena JSON en un objeto Map, que es como un diccionario (clave-valor). Dim RootMap As Map = parser.NextObject - - ' Extrae los datos necesarios del JSON. - Dim execType As String = RootMap.GetDefault("exec", "") ' Tipo de ejecución: "executeQuery" (SELECT) o "executeCommand" (INSERT/UPDATE/DELETE). - Dim queryName As String = RootMap.Get("query") ' Nombre del comando SQL (definido en config.properties). + Dim execType As String = RootMap.GetDefault("exec", "") - ' Se obtiene "params" como una Lista en lugar de un Mapa, para soportar el ordenamiento de B4A. - Dim paramsList As List = RootMap.Get("params") + queryNameForLog = RootMap.GetDefault("query", "") '[___new 3.txt, 203] + If queryNameForLog = "" Then queryNameForLog = RootMap.GetDefault("exec", "unknown_json_command") '[___new 3.txt, 203] - ' Si la lista de parámetros es nula (no se proporcionó en el JSON), la inicializamos como una lista vacía. + Dim paramsList As List = RootMap.Get("params") If paramsList = Null Or paramsList.IsInitialized = False Then paramsList.Initialize End If - ' Verifica si en el JSON se especificó un nombre de base de datos diferente con la clave "dbx". - If RootMap.Get("dbx") <> Null Then DB = RootMap.Get("dbx") ' Si se especifica, usamos la BD indicada, si no, se queda "DB1". + ' <<<< ¡CORRECCIÓN CLAVE AQUÍ: RESOLVEMOS finalDbKey del JSON ANTES! >>>> + If RootMap.Get("dbx") <> Null Then finalDbKey = RootMap.Get("dbx") '[___new 3.txt, 204] + ' <<<< ¡FIN DE CORRECCIÓN CLAVE! >>>> - ' Valida que el nombre de la base de datos (DB) exista en la lista de conexiones configuradas en Main. - If Main.listaDeCP.IndexOf(DB) = -1 Then - SendErrorResponse(resp, 400, "Parámetro 'DB' inválido. El nombre '" & DB & "' no es válido.") - Return + ' --- INICIO: Conteo de peticiones activas para esta finalDbKey (Incrementar) --- + ' 1. Aseguramos que el valor inicial sea un Int y lo recuperamos como Int. + Dim currentCountFromMap As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(finalDbKey, 0).As(Int) + GlobalParameters.ActiveRequestsCountByDB.Put(finalDbKey, currentCountFromMap + 1) + requestsBeforeDecrement = currentCountFromMap + 1 ' Este es el valor que se registra en query_logs +' Log($"[DEBUG] Handle Increment: dbKey=${finalDbKey}, currentCountFromMap=${currentCountFromMap}, requestsBeforeDecrement=${requestsBeforeDecrement}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) + ' --- FIN: Conteo de peticiones activas --- + + Connector = Main.Connectors.Get(finalDbKey) ' Inicializamos el Connector con la finalDbKey resuelta. + + + If Main.listaDeCP.IndexOf(finalDbKey) = -1 Then + SendErrorResponse(resp, 400, "Parámetro 'DB' inválido. El nombre '" & finalDbKey & "' no es válido.") + duration = DateTime.Now - start + CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return ' Salida temprana. End If - ' Obtiene una conexión a la base de datos del pool de conexiones para la DB seleccionada. - con = Connector.GetConnection(DB) + con = Connector.GetConnection(finalDbKey) ' La conexión a la BD se obtiene aquí. + + ' <<<< ¡AÑADIR ESTE RETRASO ARTIFICIAL PARA LA PRUEBA! >>>> + ' Esto forzará a C3P0 a mantener las conexiones ocupadas por más tiempo. + ' Si tienes 100 VUs, esto debería hacer que BusyConnections suba. +' Sleep(100) ' Retraso artificial de 100ms para pruebas. +' Log($"[DEBUG - ${finalDbKey}] Retraso artificial de 500ms aplicado. Pool Stats (antes de exec): Busy=${Connector.GetPoolStats.GetDefault("BusyConnections",0).As(Int)}, Total=${Connector.GetPoolStats.GetDefault("TotalConnections",0).As(Int)}"$ ) + ' <<<< ¡FIN DEL RETRASO ARTIFICIAL! >>>> + + ' <<<< BUSY_CONNECTIONS YA SE CAPTURABA BIEN. LO MANTENEMOS. >>>> + If Connector.IsInitialized Then + Dim poolStats As Map = Connector.GetPoolStats '[___new 3.txt, 204] + If poolStats.ContainsKey("BusyConnections") Then + poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Aseguramos que sea Int. + End If + End If + ' <<<< FIN DE CAPTURA! >>>> - ' Obtiene la cadena SQL del archivo de configuración usando el nombre de la consulta (queryName). - Dim sqlCommand As String = Connector.GetCommand(DB, queryName) + Dim sqlCommand As String = Connector.GetCommand(finalDbKey, queryNameForLog) - ' <<< INICIO VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE >>> If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then - Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ + Dim errorMessage As String = $"El comando '${queryNameForLog}' no fue encontrado en el config.properties de '${finalDbKey}'."$ Log(errorMessage) SendErrorResponse(resp, 400, errorMessage) - If con <> Null And con.IsInitialized Then con.Close - Return + duration = DateTime.Now - start + CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return ' Salida temprana. End If - ' <<< FIN VALIDACIÓN >>> - ' Comprueba el tipo de ejecución solicitado ("executeQuery" o "executeCommand"). If execType.ToLowerCase = "executequery" Then Dim rs As ResultSet If sqlCommand.Contains("?") Or paramsList.Size > 0 Then - ' ================================================================= - ' === VALIDACIÓN DE CONTEO DE PARÁMETROS ========================== - ' ================================================================= Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length Dim receivedParams As Int = paramsList.Size Log($"expectedParams: ${expectedParams}, receivedParams: ${receivedParams}"$) If expectedParams <> receivedParams Then - SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$) - If con <> Null And con.IsInitialized Then con.Close - Return + SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryNameForLog}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$) + duration = DateTime.Now - start + CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return ' Salida temprana. End If - ' ================================================================= rs = con.ExecQuery2(sqlCommand, paramsList) Else rs = con.ExecQuery(sqlCommand) End If - ' --- Procesamiento de resultados --- Dim ResultList As List ResultList.Initialize Dim jrs As JavaObject = rs Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) Dim cols As Int = rsmd.RunMethod("getColumnCount", Null) + Do While rs.NextRow Dim RowMap As Map RowMap.Initialize @@ -149,35 +156,60 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) Loop rs.Close SendSuccessResponse(resp, CreateMap("result": ResultList)) + Else If execType.ToLowerCase = "executecommand" Then If sqlCommand.Contains("?") Then - ' ================================================================= - ' === VALIDACIÓN DE CONTEO DE PARÁMETROS (para Comandos) ========== - ' ================================================================= Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length Dim receivedParams As Int = paramsList.Size If expectedParams <> receivedParams Then - SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$) - If con <> Null And con.IsInitialized Then con.Close - Return + SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryNameForLog}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$) + duration = DateTime.Now - start + CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return ' Salida temprana. End If - ' ================================================================= End If con.ExecNonQuery2(sqlCommand, paramsList) SendSuccessResponse(resp, CreateMap("message": "Command executed successfully")) + Else SendErrorResponse(resp, 400, "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido.") + ' El flujo continúa hasta la limpieza final si no hay un Return explícito. End If - Catch + + Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL/JSON --- Log(LastException) SendErrorResponse(resp, 500, LastException.Message) - End Try + queryNameForLog = "error_processing_json" ' Para registrar que hubo un error en el log. + End Try ' --- FIN: Bloque Try principal --- + + ' --- Lógica de logging y limpieza final (para rutas de ejecución normal o después de Catch) --- + duration = DateTime.Now - start + CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) - If con <> Null And con.IsInitialized Then - con.Close - End If End Sub +' --- NUEVA SUBRUTINA: Centraliza el logging y la limpieza --- +Private Sub CleanupAndLog(dbKey As String, qName As String, durMs As Long, clientIp As String, handlerReqs As Int, poolBusyConns As Int, conn As SQL) +' Log($"[DEBUG] CleanupAndLog Entry: dbKey=${dbKey}, handlerReqs=${handlerReqs}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) + ' 1. Llama a la subrutina centralizada para registrar el rendimiento. + Main.LogQueryPerformance(qName, durMs, dbKey, clientIp, handlerReqs, poolBusyConns) '[___new 3.txt, 207] + + ' <<<< ¡CORRECCIÓN CLAVE AQUÍ: Aseguramos que currentCount sea Int! >>>> + Dim currentCount As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0).As(Int) +' Log($"[DEBUG] CleanupAndLog Before Decrement: dbKey=${dbKey}, currentCount (as Int)=${currentCount}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) + + If currentCount > 0 Then + GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1) + Else + Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$) + GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0) + End If +' Log($"[DEBUG] CleanupAndLog After Decrement: dbKey=${dbKey}, New count (as Int)=${GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey,0).As(Int)}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) + ' <<<< ¡FIN DE CORRECCIÓN CLAVE! >>>> + + ' 3. Asegura que la conexión a la BD siempre se cierre y se devuelva al pool. + If conn <> Null And conn.IsInitialized Then conn.Close +End Sub ' --- Subrutinas de ayuda para respuestas JSON --- diff --git a/DoLoginHandler.bas b/DoLoginHandler.bas index bb21d58..2acab78 100644 --- a/DoLoginHandler.bas +++ b/DoLoginHandler.bas @@ -10,7 +10,7 @@ Sub Class_Globals End Sub Public Sub Initialize -' bc.Initialize + bc.Initialize("BC") End Sub Public Sub Handle(req As ServletRequest, resp As ServletResponse) diff --git a/GlobalParameters.bas b/GlobalParameters.bas index 67a50c2..705f782 100644 --- a/GlobalParameters.bas +++ b/GlobalParameters.bas @@ -14,4 +14,5 @@ Sub Process_Globals Public mpTotalRequests As Map Public mpTotalConnections As Map Public mpBlockConnection As Map + Public ActiveRequestsCountByDB As Map ' Mapa para contar las peticiones activas por DB End Sub \ No newline at end of file diff --git a/Manager.bas b/Manager.bas index 93c1f3c..e660b09 100644 --- a/Manager.bas +++ b/Manager.bas @@ -192,6 +192,35 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) ' Si la recarga falló, los conectores antiguos (oldConnectors) se mantienen activos ' y siguen sirviendo para evitar un paro del servicio. End If + Else If Command = "slowqueries" Then ' <<< INICIO: NUEVA Lógica para mostrar las queries lentas + sb.Append("

Consultas Lentas Recientes

") + Try + ' Ajusta la consulta SQL para obtener las 20 queries más lentas. + ' Utilizamos datetime con 'unixepoch' y 'localtime' para una visualización legible del timestamp. + Dim rs As ResultSet = Main.SQL1.ExecQuery("SELECT query_name, duration_ms, datetime(timestamp / 1000, 'unixepoch', 'localtime') as timestamp_local, db_key, client_ip, busy_connections, handler_active_requests FROM query_logs ORDER BY duration_ms DESC LIMIT 20") + + sb.Append("") + sb.Append("") + sb.Append("") + + Do While rs.NextRow + sb.Append("") + sb.Append($""$) + sb.Append($""$) + sb.Append($""$) + sb.Append($""$) + sb.Append($""$) + sb.Append($""$) + sb.Append($""$) + sb.Append("") + Loop + sb.Append("") + sb.Append("
QueryDuración (ms)Fecha/Hora LocalDB KeyCliente IPConex. OcupadasPeticiones Activas
${rs.GetString("query_name")}${rs.GetLong("duration_ms")}${rs.GetString("timestamp_local")}${rs.GetString("db_key")}${rs.GetString("client_ip")}${rs.GetInt("busy_connections")}${rs.GetInt("handler_active_requests")}
") + rs.Close + Catch + Log("Error al obtener queries lentas en Manager: " & LastException.Message) + sb.Append($"

Error al cargar queries lentas: ${LastException.Message}

"$) + End Try Else If Command = "test" Then Try Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("") diff --git a/RDCConnector.bas b/RDCConnector.bas index 24cde4a..88bb033 100644 --- a/RDCConnector.bas +++ b/RDCConnector.bas @@ -168,12 +168,34 @@ 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. +' If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB) +' Return pool.GetConnection ' Retorna una conexión del pool. +'End Sub + 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. - If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB) - Return pool.GetConnection ' Retorna una conexión del pool. + If DB.EqualsIgnoreCase("DB1") Then DB = "" + +' If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB) ' Esta línea es condicional a DebugQueries + + ' <<<< ¡ESTOS SON LOS LOGS QUE NECESITAMOS VER! >>>> +' 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 + Dim jo As JavaObject = pool + ' Aseguramos que los valores sean Ints, manejando posible retorno 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 DE LOS LOGS A BUSCAR! >>>> + + Return conn End Sub ' Carga todos los comandos SQL del mapa de configuración en el mapa global 'commandsMap'. @@ -244,11 +266,11 @@ Public Sub GetPoolStats As Map ' Log($"RDCConnector.GetPoolStats: CheckoutTimeout = ${checkoutTime}"$) Catch - Log("RDCConnector.GetPoolStats: ERROR CRÍTICO al obtener estadísticas del pool: " & LastException.Message) +' Log("RDCConnector.GetPoolStats: ERROR CRÍTICO al obtener estadísticas del pool: " & LastException.Message) stats.Put("Error", LastException.Message) End Try Else - Log("RDCConnector.GetPoolStats: ADVERTENCIA: Pool NO está inicializado. Retornando mapa con error.") +' Log("RDCConnector.GetPoolStats: ADVERTENCIA: Pool NO está inicializado. Retornando mapa con error.") stats.Put("Error", "Pool de conexiones no inicializado para esta DB.") End If @@ -265,7 +287,7 @@ End Sub ' cuando un conector RDC ya no es necesario o va a ser reemplazado. Public Sub Close If pool <> Null And pool.IsInitialized Then - Log($"RDCConnector: Cerrando pool de conexiones."$) +' Log($"RDCConnector: Cerrando pool de conexiones."$) ' 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. Dim joPool As JavaObject = pool diff --git a/jRDC_Multi.b4j b/jRDC_Multi.b4j index c4e7170..264eb38 100644 --- a/jRDC_Multi.b4j +++ b/jRDC_Multi.b4j @@ -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 \ No newline at end of file diff --git a/jRDC_Multi.b4j.meta b/jRDC_Multi.b4j.meta index 9d55938..251e29d 100644 --- a/jRDC_Multi.b4j.meta +++ b/jRDC_Multi.b4j.meta @@ -40,6 +40,6 @@ ModuleClosedNodes6= ModuleClosedNodes7= ModuleClosedNodes8= ModuleClosedNodes9= -NavigationStack=RDCConnector,Class_Globals,12,0,Manager,Initialize,8,0,RDCConnector,GetPoolStats,256,0,Main,Process_Globals,58,0,RDCConnector,Close,266,0,Manager,Handle,171,2,Main,AppStart,138,0,DBHandlerJSON,Handle,82,0,RDCConnector,Initialize,70,0,Cambios,Process_Globals,43,6 +NavigationStack=RDCConnector,GetCommand,164,0,RDCConnector,GetConnection,187,0,RDCConnector,GetPoolStats,273,0,RDCConnector,Close,283,0,DBHandlerJSON,Handle,94,5,DBHandlerJSON,CleanupAndLog,200,6,DBHandlerJSON,Initialize,10,0,DBHandlerJSON,Class_Globals,6,0,Cambios,Process_Globals,25,3,Main,AppStart,152,1 SelectedBuild=0 -VisibleModules=3,4,12,10,13,1 +VisibleModules=3,4,12,1,7,2,5