Files
jRDC-Multi/DBHandlerJSON.bas
Jose Alberto Guerra Ugalde 820fe9fc2b - VERSION 5.09.17
- fix(handlers, logs): Reporte robusto de AffectedRows (simbólico) y limpieza de tabla de errores
- Aborda dos problemas críticos para la estabilidad y fiabilidad del servidor: el manejo del conteo de filas afectadas en DMLs y la gestión del crecimiento de la tabla de logs de errores.

- Cambios Principales:

1. Fix AffectedRows (ExecuteBatch V1 y DBHandlerJSON): Dada la imposibilidad de capturar el conteo de filas afectadas real (Null) de forma segura o la falla total en tiempo de ejecución (Method: ExecNonQuery2 not matched) al usar reflexión, se revierte la lógica a la llamada directa de ExecNonQuery2. Si el comando DML se ejecuta sin lanzar una excepción SQL, se reporta simbólicamente '1' fila afectada al cliente (en el Protocolo V1 y en la respuesta JSON para executecommand) para confirmar el éxito de la operación.
2. Limpieza de Tabla de Errores: Se corrigió la subrutina Main.borraArribaDe15000Logs para incluir la tabla `errores` en la limpieza periódica. Esto asegura que el log de errores no crezca indefinidamente, manteniendo solo los 15,000 registros más recientes y realizando la optimización de espacio en disco con `vacuum`.
2025-09-23 00:16:03 -06:00

291 lines
17 KiB
QBasic

B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
' Módulo de clase: DBHandlerJSON
' Este handler se encarga de procesar las peticiones HTTP que esperan o envían datos en formato JSON.
' Es ideal para clientes web (JavaScript, axios, etc.) o servicios que interactúan con el servidor
' mediante un API RESTful. Soporta tanto GET con JSON en un parámetro 'j' como POST con JSON
' en el cuerpo de la petición.
Sub Class_Globals
' Declara una variable privada para mantener una instancia del conector RDC.
' Este objeto maneja la comunicación con la base de datos específica de la petición.
Private Connector As RDCConnector
End Sub
' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase.
Public Sub Initialize
' No se requiere inicialización específica para esta clase en este momento.
End Sub
' 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 esenciales para permitir que aplicaciones web (clientes)
' alojadas en diferentes dominios puedan comunicarse con 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.
' Las peticiones OPTIONS son pre-vuelos de CORS y no deben procesar lógica de negocio ni contadores.
If req.Method = "OPTIONS" Then
Return ' Salimos directamente para estas peticiones.
End If
Dim start As Long = DateTime.Now ' Registra el tiempo de inicio de la petición para calcular la duración.
' Declaraciones de variables con alcance en toda la subrutina para asegurar la limpieza final.
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 total 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" ' Identificador de la base de datos, con valor por defecto "DB1".
Dim requestsBeforeDecrement As Int = 0 ' Contador de peticiones activas antes de decrementar, inicializado en 0.
Dim Total As Int = 0
Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler ---
Dim jsonString As String
' <<<< INICIO: Lógica para manejar peticiones POST con JSON en el cuerpo >>>>
If req.Method = "POST" And req.ContentType.Contains("application/json") Then
' Si es un POST con JSON en el cuerpo, leemos directamente del InputStream.
Dim Is0 As InputStream = req.InputStream
Dim bytes() As Byte = Bit.InputStreamToBytes(Is0) ' Lee el cuerpo completo de la petición.
jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") ' Convierte los bytes a una cadena JSON.
Is0.Close ' Cierra explícitamente el InputStream para liberar recursos.
Else
' De lo contrario, asumimos que el JSON viene en el parámetro 'j' de la URL (método legacy/GET).
jsonString = req.GetParameter("j")
End If
' <<<< FIN: Lógica para manejar peticiones POST con JSON en el cuerpo >>>>
' Validación inicial: Si no hay JSON, se envía un error 400.
If jsonString = Null Or jsonString = "" Then
Dim ErrorMsg As String = "Falta el parámetro 'j' en el URL o el cuerpo JSON en la petición."
SendErrorResponse(resp, 400, ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
Dim parser As JSONParser
parser.Initialize(jsonString) ' Inicializa el parser JSON con la cadena recibida.
Dim RootMap As Map = parser.NextObject ' Parsea el JSON a un objeto Map.
Dim execType As String = RootMap.GetDefault("exec", "") ' Obtiene el tipo de ejecución (ej. "ExecuteQuery").
' Obtiene el nombre de la query. Si no está en "query", busca en "exec".
queryNameForLog = RootMap.GetDefault("query", "")
If queryNameForLog = "" Then queryNameForLog = RootMap.GetDefault("exec", "unknown_json_command")
Dim paramsList As List = RootMap.Get("params") ' Obtiene la lista de parámetros para la query.
If paramsList = Null Or paramsList.IsInitialized = False Then
paramsList.Initialize ' Si no hay parámetros, inicializa una lista vacía.
End If
' <<<< ¡CORRECCIÓN CLAVE: RESOLVEMOS finalDbKey del JSON ANTES de usarla para los contadores! >>>>
' Esto asegura que el contador y el conector usen la DB correcta.
If RootMap.Get("dbx") <> Null Then finalDbKey = RootMap.Get("dbx")
' <<<< ¡FIN DE CORRECCIÓN CLAVE! >>>>
' --- INICIO: Conteo de peticiones activas para esta finalDbKey (Incrementar) ---
' Este bloque incrementa un contador global que rastrea cuántas peticiones están
' activas para una base de datos específica en un momento dado.
' 1. Aseguramos que el valor inicial sea un Int y lo recuperamos como Int (usando .As(Int)).
Dim currentCountFromMap As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(finalDbKey, 0).As(Int)
GlobalParameters.ActiveRequestsCountByDB.Put(finalDbKey, currentCountFromMap + 1)
' requestsBeforeDecrement es el valor del contador justo después de que esta petición lo incrementa.
' Este es el valor que se registrará en la tabla 'query_logs'.
requestsBeforeDecrement = currentCountFromMap + 1
' Los logs de depuración para el incremento del contador pueden ser descomentados para una depuración profunda.
' Log($"[DEBUG] Handle Increment (JSON): dbKey=${finalDbKey}, currentCountFromMap=${currentCountFromMap}, requestsBeforeDecrement=${requestsBeforeDecrement}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
' --- FIN: Conteo de peticiones activas ---
' Inicializa el Connector con la finalDbKey resuelta.
Connector = Main.Connectors.Get(finalDbKey)
' Validación: Si el dbKey no es válido o no está configurado en Main.listaDeCP.
If Main.listaDeCP.IndexOf(finalDbKey) = -1 Then
Dim ErrorMsg As String = "Parámetro 'DB' inválido. El nombre '" & finalDbKey & "' no es válido."
SendErrorResponse(resp, 400, ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
con = Connector.GetConnection(finalDbKey) ' ¡La conexión a la BD se obtiene aquí del pool de conexiones!
' <<<< ¡CAPTURAMOS BUSY_CONNECTIONS INMEDIATAMENTE DESPUÉS DE OBTENER LA CONEXIÓN! >>>>
' Este bloque captura el número de conexiones actualmente ocupadas en el pool
' *después* de que esta petición ha obtenido la suya.
If Connector.IsInitialized Then
Dim poolStats As Map = Connector.GetPoolStats
If poolStats.ContainsKey("BusyConnections") Then
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>>
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor.
End If
End If
' <<<< ¡FIN DE CAPTURA! >>>>
' Obtiene la sentencia SQL correspondiente al nombre del comando desde config.properties.
Dim sqlCommand As String = Connector.GetCommand(finalDbKey, queryNameForLog)
' Validación: Si el comando SQL no fue encontrado en la configuración.
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
Dim errorMessage As String = $"El comando '${queryNameForLog}' no fue encontrado en el config.properties de '${finalDbKey}'."$
Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", errorMessage, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
SendErrorResponse(resp, 400, errorMessage)
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
' --- Lógica para ejecutar diferentes tipos de comandos basados en el parámetro 'execType' ---
If execType.ToLowerCase = "executequery" Then
' --- INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryNameForLog, finalDbKey, sqlCommand, paramsList, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
SendErrorResponse(resp, 400, validationResult.ErrorMessage)
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana.
End If
Dim rs As ResultSet
' Ejecuta la consulta SQL con la lista de parámetros validada.
rs = con.ExecQuery2(sqlCommand, validationResult.ParamsToExecute)
' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
Dim ResultList As List
ResultList.Initialize ' Lista para almacenar los resultados de la consulta.
Dim jrs As JavaObject = rs ' Objeto Java subyacente del ResultSet para metadatos.
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) ' Metadatos del ResultSet.
Dim cols As Int = rsmd.RunMethod("getColumnCount", Null) ' Número de columnas.
Do While rs.NextRow ' Itera sobre cada fila del resultado.
Dim RowMap As Map
RowMap.Initialize ' Mapa para almacenar los datos de la fila actual.
For i = 1 To cols ' Itera sobre cada columna.
Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i)) ' Nombre de la columna.
Dim value As Object = jrs.RunMethod("getObject", Array(i)) ' Valor de la columna.
RowMap.Put(ColumnName, value) ' Añade la columna y su valor al mapa de la fila.
Next
ResultList.Add(RowMap) ' Añade el mapa de la fila a la lista de resultados.
Loop
rs.Close ' Cierra el ResultSet.
SendSuccessResponse(resp, CreateMap("result": ResultList)) ' Envía la respuesta JSON de éxito.
Else If execType.ToLowerCase = "executecommand" Then
' --- INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryNameForLog, finalDbKey, sqlCommand, paramsList, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
SendErrorResponse(resp, 400, validationResult.ErrorMessage)
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana.
End If
Dim affectedCount As Int = 1 ' Asumimos éxito (1) si ExecNonQuery2 no lanza una excepción.
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta un comando con la lista de parámetros validada.
SendSuccessResponse(resp, CreateMap("affectedRows": affectedCount, "message": "Command executed successfully")) ' Envía confirmación de éxito.
' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
Else
Dim ErrorMsg As String = "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido."
SendErrorResponse(resp, 400, ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
' El flujo continúa hasta la limpieza final si no hay un Return explícito.
End If
Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL/JSON ---
Log(LastException) ' Registra la excepción completa en el log.
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", LastException.Message, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
SendErrorResponse(resp, 500, LastException.Message) ' Envía un error 500 al cliente.
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) ---
' Este bloque se asegura de que, independientemente de cómo termine la petición (éxito o error),
' la duración se calcule y se llamen las subrutinas de limpieza y logging.
duration = DateTime.Now - start ' Calcula la duración total de la petición.
' Llama a la subrutina centralizada para registrar el rendimiento y limpiar recursos.
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
End Sub
' --- NUEVA SUBRUTINA: Centraliza el logging de rendimiento y la limpieza de recursos ---
' Esta subrutina es llamada por Handle en todos los puntos de salida, asegurando
' que los contadores se decrementen y las conexiones se cierren de forma consistente.
Private Sub CleanupAndLog(dbKey As String, qName As String, durMs As Long, clientIp As String, handlerReqs As Int, poolBusyConns As Int, conn As SQL)
' Los logs de depuración para CleanupAndLog pueden ser descomentados para una depuración profunda.
' Log($"[DEBUG] CleanupAndLog Entry (JSON): dbKey=${dbKey}, handlerReqs=${handlerReqs}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
' 1. Llama a la subrutina centralizada en Main para registrar el rendimiento en SQLite.
Main.LogQueryPerformance(qName, durMs, dbKey, clientIp, handlerReqs, poolBusyConns)
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que currentCount sea Int al obtenerlo del mapa! >>>>
' 2. Decrementa el contador de peticiones activas para esta dbKey de forma robusta.
Dim currentCount As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0).As(Int)
' Log($"[DEBUG] CleanupAndLog Before Decrement (JSON): dbKey=${dbKey}, currentCount (as Int)=${currentCount}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
If currentCount > 0 Then
' Si el contador es positivo, lo decrementamos.
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1)
Else
' Si el contador ya está en 0 o negativo (lo cual no debería ocurrir con la lógica actual,
' pero se maneja para robustez), 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
' Log($"[DEBUG] CleanupAndLog After Decrement (JSON): 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 de conexiones.
If conn <> Null And conn.IsInitialized Then conn.Close
End Sub
' --- Subrutinas de ayuda para respuestas JSON ---
' Construye y envía una respuesta JSON de éxito.
' resp: El objeto ServletResponse para enviar la respuesta.
' dataMap: Un mapa que contiene los datos a incluir en la respuesta JSON.
Private Sub SendSuccessResponse(resp As ServletResponse, dataMap As Map)
' Añade el campo "success": true al mapa de datos para indicar que todo salió bien.
dataMap.Put("success", True)
' Crea un generador de JSON.
Dim jsonGenerator As JSONGenerator
jsonGenerator.Initialize(dataMap)
' Establece el tipo de contenido de la respuesta a "application/json".
resp.ContentType = "application/json"
' Escribe la cadena JSON generada en el cuerpo de la respuesta HTTP.
resp.Write(jsonGenerator.ToString)
End Sub
' Construye y envía una respuesta JSON de error.
' resp: El objeto ServletResponse para enviar la respuesta.
' statusCode: El código de estado HTTP (ej. 400 para error del cliente, 500 para error del servidor).
' errorMessage: El mensaje de error que se enviará al cliente.
Private Sub SendErrorResponse(resp As ServletResponse, statusCode As Int, errorMessage As String)
' Personaliza el mensaje de error si es un error común de parámetros de Oracle o JDBC.
If errorMessage.Contains("Índice de columnas no válido") Or errorMessage.Contains("ORA-17003") Then
errorMessage = "NUMERO DE PARAMETROS EQUIVOCADO: " & errorMessage
End If
' Crea un mapa con el estado de error y el mensaje.
Dim resMap As Map = CreateMap("success": False, "error": errorMessage)
' Genera la cadena JSON a partir del mapa.
Dim jsonGenerator As JSONGenerator
jsonGenerator.Initialize(resMap)
' Establece el código de estado HTTP (ej. 400 para error del cliente, 500 para error del servidor).
resp.Status = statusCode
' Establece el tipo de contenido y escribe la respuesta de error.
resp.ContentType = "application/json"
resp.Write(jsonGenerator.ToString)
End Sub