- 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

@@ -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'.

View File

@@ -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("<script>alert('Error: La contraseña actual es incorrecta.'); history.back();</script>")
Return
End If

View File

@@ -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).

View File

@@ -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 ---

View File

@@ -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)

View File

@@ -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

View File

@@ -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("<h2>Consultas Lentas Recientes</h2>")
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("<table border='1' style='width:100%; text-align:left; border-collapse: collapse;'>")
sb.Append("<thead><tr><th>Query</th><th>Duración (ms)</th><th>Fecha/Hora Local</th><th>DB Key</th><th>Cliente IP</th><th>Conex. Ocupadas</th><th>Peticiones Activas</th></tr></thead>")
sb.Append("<tbody>")
Do While rs.NextRow
sb.Append("<tr>")
sb.Append($"<td>${rs.GetString("query_name")}</td>"$)
sb.Append($"<td>${rs.GetLong("duration_ms")}</td>"$)
sb.Append($"<td>${rs.GetString("timestamp_local")}</td>"$)
sb.Append($"<td>${rs.GetString("db_key")}</td>"$)
sb.Append($"<td>${rs.GetString("client_ip")}</td>"$)
sb.Append($"<td>${rs.GetInt("busy_connections")}</td>"$)
sb.Append($"<td>${rs.GetInt("handler_active_requests")}</td>"$)
sb.Append("</tr>")
Loop
sb.Append("</tbody>")
sb.Append("</table>")
rs.Close
Catch
Log("Error al obtener queries lentas en Manager: " & LastException.Message)
sb.Append($"<p style='color:red;'>Error al cargar queries lentas: ${LastException.Message}</p>"$)
End Try
Else If Command = "test" Then
Try
Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("")

View File

@@ -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

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

View File

@@ -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