mirror of
https://github.com/KeymonSoft/jRDC-Multi.git
synced 2026-04-17 21:06:24 +00:00
- 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:
@@ -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 ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user