- 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 una forma de probar con carga el servidor
'- Agregar la opcion de "Queries lentos" '- 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 '- VERSION 5.09.13.3
'- Implementación de "Hot-Swap" para recarga de configuraciones de DB sin reiniciar el servidor. '- 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'. '- 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("--- Probando con contraseña fija ---")
Log("Valor de la BD (storedHash): " & storedHash) 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>") resp.Write("<script>alert('Error: La contraseña actual es incorrecta.'); history.back();</script>")
Return Return
End If End If

View File

@@ -35,100 +35,136 @@ End Sub
' Método principal que maneja cada petición HTTP que llega a este servlet. ' Método principal que maneja cada petición HTTP que llega a este servlet.
Sub Handle(req As ServletRequest, resp As ServletResponse) Sub Handle(req As ServletRequest, resp As ServletResponse)
' === INICIO DE LA LÓGICA DINÁMICA === ' === INICIO DE LA LÓGICA DINÁMICA (Extracción de dbKey de la URL) ===
' Extrae la URI completa de la petición (ej. /DB1/endpoint).
Dim URI As String = req.RequestURI Dim URI As String = req.RequestURI
' Variable para almacenar la "llave" o identificador de la base de datos (ej. "DB1"). Dim dbKey As String ' Usamos dbKey para consistencia con tu código original.
Dim dbKey As String
' Comprueba si la URI tiene contenido y empieza con "/".
If URI.Length > 1 And URI.StartsWith("/") Then If URI.Length > 1 And URI.StartsWith("/") Then
' Extrae la parte de la URI que viene después del primer "/". dbKey = URI.Substring(1) '[DBHandlerB4X.bas.txt, 51]
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".
If dbKey.Contains("/") Then If dbKey.Contains("/") Then
dbKey = dbKey.SubString2(0, dbKey.IndexOf("/")) dbKey = dbKey.SubString2(0, dbKey.IndexOf("/")) '[DBHandlerB4X.bas.txt, 51]
End If End If
Else Else
' Si la URI está vacía o es "/", usa "DB1" como la base de datos por defecto. dbKey = "DB1" '[DBHandlerB4X.bas.txt, 51]
dbKey = "DB1"
End If 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"). If Main.Connectors.ContainsKey(dbKey) = False Then '[DBHandlerB4X.bas.txt, 52]
dbKey = dbKey.ToUpperCase 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]
' Verifica si la llave de la base de datos extraída existe en la configuración de conectores. SendPlainTextError(resp, 400, ErrorMsg) '[DBHandlerB4X.bas.txt, 52]
If Main.Connectors.ContainsKey(dbKey) = False Then ' Aquí no se necesita CleanupAndLog, ya que el contador no se ha incrementado
' Si no existe, crea un mensaje de error claro. ' y no se ha obtenido ninguna conexión del pool aún.
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.
Return Return
End If End If
' === FIN DE LA LÓGICA DINÁMICA === ' === FIN DE LA LÓGICA DINÁMICA ===
Log("********************* " & dbKey & " ********************") Log("********************* " & dbKey & " ********************") '[DBHandlerB4X.bas.txt, 53]
' Guarda el tiempo de inicio para medir la duración de la petición.
Dim start As Long = DateTime.Now Dim start As Long = DateTime.Now '[___new 3.txt, 203]
' Variable para almacenar el nombre del comando SQL a ejecutar.
Dim q As String ' --- INICIO: Conteo de peticiones activas para esta dbKey (Incrementar) ---
' Obtiene el stream de entrada de la petición, que contiene los datos enviados por el cliente. Dim currentActiveRequests As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0) '[___new 3.txt, 205]
Dim in As InputStream = req.InputStream GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentActiveRequests + 1) '[___new 3.txt, 205]
' Obtiene el parámetro "method" de la URL (ej. ?method=query2). Dim requestsBeforeDecrement As Int = currentActiveRequests + 1 '[___new 3.txt, 207]
Dim method As String = req.GetParameter("method") ' --- FIN: Conteo de peticiones activas ---
' Obtiene el conector correspondiente a la base de datos seleccionada.
Connector = Main.Connectors.Get(dbKey) ' Declaraciones de variables con alcance en toda la subrutina para la limpieza.
' Declara la variable para la conexión a la base de datos. Dim q As String = "unknown_b4x_command" ' Nombre del comando para el log, con valor por defecto.
Dim con As SQL Dim con As SQL ' La conexión a la BD, se inicializará más tarde.
Try Dim duration As Long ' La duración de la petición, calculada antes del log.
' Obtiene una conexión del pool de conexiones. Dim poolBusyConnectionsForLog As Int = 0 ' Contiene el número de conexiones ocupadas del pool.
con = Connector.GetConnection(dbKey)
Log("Metodo: " & method) Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler ---
' Determina qué función ejecutar basándose en el parámetro "method". 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 If method = "query2" Then
' Ejecuta una consulta usando el protocolo más nuevo (B4XSerializator). q = ExecuteQuery2(dbKey, con, in, resp) '[DBHandlerB4X.bas.txt, 54]
q = ExecuteQuery2(dbKey, con, in, resp) 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 '#if VERSION1
Else if method = "query" Then Else if method = "query" Then
' Protocolo antiguo: descomprime el stream y ejecuta la consulta.
in = cs.WrapInputStream(in, "gzip") 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 Else if method = "batch" Then
' Protocolo antiguo: descomprime el stream y ejecuta un lote de comandos.
in = cs.WrapInputStream(in, "gzip") 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 '#end if
Else if method = "batch2" Then Else if method = "batch2" Then
' Ejecuta un lote de comandos usando el protocolo más nuevo. q = ExecuteBatch2(dbKey, con, in, resp) '[DBHandlerB4X.bas.txt, 55]
q = ExecuteBatch2(dbKey, con, in, resp) If q = "error" Then
duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
Else Else
' Si el método es desconocido, lo registra y envía un error. Log("Unknown method: " & method) '[DBHandlerB4X.bas.txt, 56]
Log("Unknown method: " & method) SendPlainTextError(resp, 500, "unknown method") '[DBHandlerB4X.bas.txt, 56]
SendPlainTextError(resp, 500, "unknown method") 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 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 *** Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL ---
Dim duration As Long = DateTime.Now - start Log(LastException) '[DBHandlerB4X.bas.txt, 56]
Try SendPlainTextError(resp, 500, LastException.Message) '[DBHandlerB4X.bas.txt, 56]
Main.SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip) VALUES (?, ?, ?, ?, ?)", _ q = "error_in_b4x_handler" ' Aseguramos un valor para q en el log si hay excepción.
Array As Object(q, duration, DateTime.Now, dbKey, req.RemoteAddress)) End Try ' --- FIN: Bloque Try principal ---
Catch
Log("Error al guardar log de query en SQLite (DBHandlerB4X): " & LastException.Message) ' --- Lógica de logging y limpieza final (para rutas de ejecución normal o después de Catch) ---
End Try 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 End Sub
' Ejecuta una consulta única usando el protocolo V2 (B4XSerializator). ' 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).
' 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) Sub Handle(req As ServletRequest, resp As ServletResponse)
' --- Headers CORS (Cross-Origin Resource Sharing) --- ' --- Headers CORS (Cross-Origin Resource Sharing) ---
' Estos encabezados son necesarios para permitir que un cliente web (ej. una página con JavaScript) resp.SetHeader("Access-Control-Allow-Origin", "*")
' que se encuentra en un dominio diferente pueda hacer peticiones a este servidor. resp.SetHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
resp.SetHeader("Access-Control-Allow-Origin", "*") ' Permite peticiones desde cualquier origen. resp.SetHeader("Access-Control-Allow-Headers", "Content-Type")
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.
' El método OPTIONS es una "petición de comprobación previa" (preflight request) que envían los navegadores If req.Method = "OPTIONS" Then
' para verificar los permisos CORS antes de enviar la petición real (ej. POST). Return ' Las peticiones OPTIONS no incrementan contadores ni usan BD, así que salimos directamente.
' Si es una petición OPTIONS, simplemente terminamos la ejecución sin procesar nada más. End If
If req.Method = "OPTIONS" Then Return
' Establece "DB1" como el nombre de la base de datos por defecto. Dim start As Long = DateTime.Now
Dim DB As String = "DB1"
' Obtiene el objeto conector para la base de datos por defecto o especificada. ' Declaraciones de variables con alcance en toda la subrutina para la limpieza.
Connector = Main.Connectors.Get(DB) 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. Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler ---
Dim con As SQL
' Inicia un bloque Try...Catch para manejar posibles errores durante la ejecución.
Try
Dim jsonString As String 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 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
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 bytes() As Byte = Bit.InputStreamToBytes(Is0) Dim bytes() As Byte = Bit.InputStreamToBytes(Is0)
jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") ' Convertimos los bytes a String UTF8. jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8")
Is0.Close ' ¡Es CRÍTICO cerrar el InputStream para liberar los recursos del sistema! Is0.Close
Else 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") jsonString = req.GetParameter("j")
End If 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 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.") 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 End If
' Crea un objeto JSONParser para analizar la cadena JSON.
Dim parser As JSONParser Dim parser As JSONParser
parser.Initialize(jsonString) parser.Initialize(jsonString)
' Convierte la cadena JSON en un objeto Map, que es como un diccionario (clave-valor).
Dim RootMap As Map = parser.NextObject Dim RootMap As Map = parser.NextObject
Dim execType As String = RootMap.GetDefault("exec", "")
' 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).
' Se obtiene "params" como una Lista en lugar de un Mapa, para soportar el ordenamiento de B4A. queryNameForLog = RootMap.GetDefault("query", "") '[___new 3.txt, 203]
Dim paramsList As List = RootMap.Get("params") 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 If paramsList = Null Or paramsList.IsInitialized = False Then
paramsList.Initialize paramsList.Initialize
End If End If
' Verifica si en el JSON se especificó un nombre de base de datos diferente con la clave "dbx". ' <<<< ¡CORRECCIÓN CLAVE AQUÍ: RESOLVEMOS finalDbKey del JSON ANTES! >>>>
If RootMap.Get("dbx") <> Null Then DB = RootMap.Get("dbx") ' Si se especifica, usamos la BD indicada, si no, se queda "DB1". 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. ' --- INICIO: Conteo de peticiones activas para esta finalDbKey (Incrementar) ---
If Main.listaDeCP.IndexOf(DB) = -1 Then ' 1. Aseguramos que el valor inicial sea un Int y lo recuperamos como Int.
SendErrorResponse(resp, 400, "Parámetro 'DB' inválido. El nombre '" & DB & "' no es válido.") Dim currentCountFromMap As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(finalDbKey, 0).As(Int)
Return 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 End If
' Obtiene una conexión a la base de datos del pool de conexiones para la DB seleccionada. con = Connector.GetConnection(finalDbKey) ' La conexión a la BD se obtiene aquí.
con = Connector.GetConnection(DB)
' <<<< ¡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(finalDbKey, queryNameForLog)
Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
' <<< INICIO VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE >>>
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then 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) Log(errorMessage)
SendErrorResponse(resp, 400, errorMessage) SendErrorResponse(resp, 400, errorMessage)
If con <> Null And con.IsInitialized Then con.Close duration = DateTime.Now - start
Return CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana.
End If End If
' <<< FIN VALIDACIÓN >>>
' Comprueba el tipo de ejecución solicitado ("executeQuery" o "executeCommand").
If execType.ToLowerCase = "executequery" Then If execType.ToLowerCase = "executequery" Then
Dim rs As ResultSet Dim rs As ResultSet
If sqlCommand.Contains("?") Or paramsList.Size > 0 Then 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 expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
Dim receivedParams As Int = paramsList.Size Dim receivedParams As Int = paramsList.Size
Log($"expectedParams: ${expectedParams}, receivedParams: ${receivedParams}"$) Log($"expectedParams: ${expectedParams}, receivedParams: ${receivedParams}"$)
If expectedParams <> receivedParams Then If expectedParams <> receivedParams Then
SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$) SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryNameForLog}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
If con <> Null And con.IsInitialized Then con.Close duration = DateTime.Now - start
Return CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana.
End If End If
' =================================================================
rs = con.ExecQuery2(sqlCommand, paramsList) rs = con.ExecQuery2(sqlCommand, paramsList)
Else Else
rs = con.ExecQuery(sqlCommand) rs = con.ExecQuery(sqlCommand)
End If End If
' --- Procesamiento de resultados ---
Dim ResultList As List Dim ResultList As List
ResultList.Initialize ResultList.Initialize
Dim jrs As JavaObject = rs Dim jrs As JavaObject = rs
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null)
Dim cols As Int = rsmd.RunMethod("getColumnCount", Null) Dim cols As Int = rsmd.RunMethod("getColumnCount", Null)
Do While rs.NextRow Do While rs.NextRow
Dim RowMap As Map Dim RowMap As Map
RowMap.Initialize RowMap.Initialize
@@ -149,35 +156,60 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Loop Loop
rs.Close rs.Close
SendSuccessResponse(resp, CreateMap("result": ResultList)) SendSuccessResponse(resp, CreateMap("result": ResultList))
Else If execType.ToLowerCase = "executecommand" Then Else If execType.ToLowerCase = "executecommand" Then
If sqlCommand.Contains("?") Then If sqlCommand.Contains("?") Then
' =================================================================
' === VALIDACIÓN DE CONTEO DE PARÁMETROS (para Comandos) ==========
' =================================================================
Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
Dim receivedParams As Int = paramsList.Size Dim receivedParams As Int = paramsList.Size
If expectedParams <> receivedParams Then If expectedParams <> receivedParams Then
SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$) SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryNameForLog}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
If con <> Null And con.IsInitialized Then con.Close duration = DateTime.Now - start
Return CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana.
End If End If
' =================================================================
End If End If
con.ExecNonQuery2(sqlCommand, paramsList) con.ExecNonQuery2(sqlCommand, paramsList)
SendSuccessResponse(resp, CreateMap("message": "Command executed successfully")) SendSuccessResponse(resp, CreateMap("message": "Command executed successfully"))
Else Else
SendErrorResponse(resp, 400, "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido.") 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 End If
Catch
Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL/JSON ---
Log(LastException) Log(LastException)
SendErrorResponse(resp, 500, LastException.Message) 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 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 --- ' --- Subrutinas de ayuda para respuestas JSON ---

View File

@@ -10,7 +10,7 @@ Sub Class_Globals
End Sub End Sub
Public Sub Initialize Public Sub Initialize
' bc.Initialize bc.Initialize("BC")
End Sub End Sub
Public Sub Handle(req As ServletRequest, resp As ServletResponse) Public Sub Handle(req As ServletRequest, resp As ServletResponse)

View File

@@ -14,4 +14,5 @@ Sub Process_Globals
Public mpTotalRequests As Map Public mpTotalRequests As Map
Public mpTotalConnections As Map Public mpTotalConnections As Map
Public mpBlockConnection As Map Public mpBlockConnection As Map
Public ActiveRequestsCountByDB As Map ' Mapa para contar las peticiones activas por DB
End Sub 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 ' Si la recarga falló, los conectores antiguos (oldConnectors) se mantienen activos
' y siguen sirviendo para evitar un paro del servicio. ' y siguen sirviendo para evitar un paro del servicio.
End If 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 Else If Command = "test" Then
Try Try
Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("") 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. ' Obtiene una conexión SQL funcional del pool de conexiones para la base de datos especificada.
' DB: El identificador de la base de datos. ' DB: El identificador de la base de datos.
' Retorna un objeto SQL (la conexión a 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 Public Sub GetConnection(DB As String) As SQL
If DB.EqualsIgnoreCase("DB1") Then DB = "" 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) ' Esta línea es condicional a DebugQueries
If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB)
Return pool.GetConnection ' Retorna una conexión del pool. ' <<<< ¡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 End Sub
' Carga todos los comandos SQL del mapa de configuración en el mapa global 'commandsMap'. ' 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}"$) ' Log($"RDCConnector.GetPoolStats: CheckoutTimeout = ${checkoutTime}"$)
Catch 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) stats.Put("Error", LastException.Message)
End Try End Try
Else 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.") stats.Put("Error", "Pool de conexiones no inicializado para esta DB.")
End If End If
@@ -265,7 +287,7 @@ End Sub
' cuando un conector RDC ya no es necesario o va a ser reemplazado. ' cuando un conector RDC ya no es necesario o va a ser reemplazado.
Public Sub Close Public Sub Close
If pool <> Null And pool.IsInitialized Then 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()' ' 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. ' que no está expuesto directamente en la envoltura de B4X.
Dim joPool As JavaObject = pool Dim joPool As JavaObject = pool

View File

@@ -53,7 +53,7 @@ Version=10.3
#Region Project Attributes #Region Project Attributes
#CommandLineArgs: #CommandLineArgs:
#MergeLibraries: True #MergeLibraries: True
' VERSION 5.09.014 ' VERSION 5.09.14
'########################################################################################################### '###########################################################################################################
'###################### PULL ############################################################# '###################### PULL #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=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 #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 'change based on the jdbc jar file
'#AdditionalJar: mysql-connector-java-5.1.27-bin '#AdditionalJar: mysql-connector-java-5.1.27-bin
'#AdditionalJar: postgresql-42.7.0 '#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). 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). 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 MainConnectorsLock As JavaObject ' Objeto de bloqueo para proteger Main.Connectors
Public ActiveRequestsCountByDB As Map
End Sub End Sub
Sub AppStart (Args() As String) 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.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.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.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. ' 3. Inicializa las estructuras principales del servidor HTTP.
listaDeCP.Initialize ' Inicializa la lista que contendrá los IDs de las bases de datos. 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. ' en el mismo directorio donde se ejecuta el JAR.
cpFiles = File.ListFiles("./") cpFiles = File.ListFiles("./")
If cpFiles.Size > 0 Then If cpFiles.Size > 0 Then
For i = 0 To cpFiles.Size - 1 For i = 0 To cpFiles.Size - 1
' Procesa 'config.DB2.properties' ' Procesa 'config.DB2.properties'
If cpFiles.Get(i) = "config.DB2.properties" Then If cpFiles.Get(i) = "config.DB2.properties" Then
Dim con2 As RDCConnector ' Declara una variable específica y única para el conector de DB2. 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". con2.Initialize("DB2") ' Inicializa la instancia del conector para "DB2".
Connectors.Put("DB2", con2) ' Asocia "DB2" con su instancia de RDCConnector. Connectors.Put("DB2", con2) ' Asocia "DB2" con su instancia de RDCConnector.
listaDeCP.Add("DB2") ' Añade "DB2" a la lista de bases de datos. listaDeCP.Add("DB2") ' Añade "DB2" a la lista de bases de datos.
Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.") Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.")
End If End If
' Procesa 'config.DB3.properties' ' Procesa 'config.DB3.properties'
If cpFiles.Get(i) = "config.DB3.properties" Then If cpFiles.Get(i) = "config.DB3.properties" Then
Dim con3 As RDCConnector ' Declara una variable específica y única para el conector de DB3. 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". con3.Initialize("DB3") ' Inicializa la instancia del conector para "DB3".
Connectors.Put("DB3", con3) ' Asocia "DB3" con su instancia de RDCConnector. Connectors.Put("DB3", con3) ' Asocia "DB3" con su instancia de RDCConnector.
listaDeCP.Add("DB3") ' Añade "DB3" a la lista de bases de datos. listaDeCP.Add("DB3") ' Añade "DB3" a la lista de bases de datos.
Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.") Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.")
End If End If
' Procesa 'config.DB4.properties' ' Procesa 'config.DB4.properties'
If cpFiles.Get(i) = "config.DB4.properties" Then If cpFiles.Get(i) = "config.DB4.properties" Then
Dim con4 As RDCConnector ' Declara una variable específica y única para el conector de DB4. 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". con4.Initialize("DB4") ' Inicializa la instancia del conector para "DB4".
Connectors.Put("DB4", con4) ' Asocia "DB4" con su instancia de RDCConnector. Connectors.Put("DB4", con4) ' Asocia "DB4" con su instancia de RDCConnector.
listaDeCP.Add("DB4") ' Añade "DB4" a la lista de bases de datos. listaDeCP.Add("DB4") ' Añade "DB4" a la lista de bases de datos.
Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.") Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.")
End If End If
Next Next
Else Else
Log("Main.AppStart: No se encontraron archivos de configuración adicionales (config.DBx.properties).") Log("Main.AppStart: No se encontraron archivos de configuración adicionales (config.DBx.properties).")
End If End If
' Log final de las bases de datos que el servidor está gestionando. ' Log final de las bases de datos que el servidor está gestionando.
Dim sbListaDeCP_Log As StringBuilder Dim sbListaDeCP_Log As StringBuilder
sbListaDeCP_Log.Initialize sbListaDeCP_Log.Initialize
For Each item As String In listaDeCP For Each item As String In listaDeCP
sbListaDeCP_Log.Append(item).Append(", ") sbListaDeCP_Log.Append(item).Append(", ")
Next Next
If sbListaDeCP_Log.Length > 0 Then If sbListaDeCP_Log.Length > 0 Then
sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length) ' Elimina la última ", " sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length) ' Elimina la última ", "
End If End If
Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$) Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$)
' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR === ' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR ===
' Asocia rutas URL específicas con clases que manejarán las peticiones correspondientes. ' 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, ' 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. ' 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("/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("/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("/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("/dologin", "DoLoginHandler", True) ' Procesa el intento de inicio de sesión.
srvr.AddHandler("/logout", "LogoutHandler", True) ' Cierra la sesión del usuario. 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("/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("/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("/DBJ", "DBHandlerJSON", False) ' 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("/dbrquery", "DBHandlerJSON", False) ' Un alias para el handler JSON, por si se usa en clientes específicos.
srvr.AddHandler("/favicon.ico", "faviconHandler", True) ' Sirve el icono de la página (favicon). srvr.AddHandler("/favicon.ico", "faviconHandler", True) ' Sirve el icono de la página (favicon).
srvr.AddHandler("/*", "DBHandlerB4X", True) ' Handler por defecto para clientes B4X (DBRequestManager), srvr.AddHandler("/*", "DBHandlerB4X", False) ' Handler por defecto para clientes B4X (DBRequestManager),
' procesa peticiones dinámicamente según la URL. ' procesa peticiones dinámicamente según la URL.
' 7. Inicia el servidor HTTP. ' 7. Inicia el servidor HTTP.
srvr.Start srvr.Start
Log("===========================================================") Log("===========================================================")
Log($"-=== jRDC está funcionando en el puerto: ${srvr.Port} (versión = $1.2{VERSION}) ===-"$) Log($"-=== jRDC está funcionando en el puerto: ${srvr.Port} (versión = $1.2{VERSION}) ===-"$)
Log("===========================================================") 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
' 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 End Sub
' --- Subrutina para inicializar la base de datos de usuarios local (SQLite) --- ' --- Subrutina para inicializar la base de datos de usuarios local (SQLite) ---
' Esta base de datos se utiliza para almacenar credenciales de usuarios que pueden ' 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 Sub InitializeSQLiteDatabase
Dim dbFileName As String = "users.db" ' Nombre del archivo de la base de datos SQLite. 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. ' 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 If File.Exists(File.DirApp, dbFileName) = False Then
Log("Creando nueva base de datos de usuarios: " & dbFileName) 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). ' 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) 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'. ' Primero, verificar si la tabla query_logs existe.
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)" If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then
SQL1.ExecNonQuery(createUserTable) 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)"
' Crea un usuario por defecto para facilitar el primer acceso al panel de administración. SQL1.ExecNonQuery(createQueryLogsTable)
Dim defaultUser As String = "admin" Else
Dim defaultPass As String = "12345" ' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes.
' Genera un hash seguro de la contraseña usando BCrypt, lo cual es crucial para la seguridad. Dim columnExists As Boolean
Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt) Dim rs As ResultSet
' Inserta el usuario por defecto en la tabla 'users'.
SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass)) ' --- VERIFICAR Y AÑADIR busy_connections ---
Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$) columnExists = False
Else ' Ejecutamos PRAGMA sin WHERE y lo filtramos en código.
' Si el archivo de la base de datos ya existe, simplemente se abre. rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
SQL1.InitializeSQLite(File.DirApp, dbFileName, True) Do While rs.NextRow
Log("Base de datos de usuarios cargada.") If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then
End If 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 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= ModuleClosedNodes7=
ModuleClosedNodes8= ModuleClosedNodes8=
ModuleClosedNodes9= 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 SelectedBuild=0
VisibleModules=3,4,12,10,13,1 VisibleModules=3,4,12,1,7,2,5