diff --git a/Cambios.bas b/Cambios.bas index 6c3213f..bbd64e0 100644 --- a/Cambios.bas +++ b/Cambios.bas @@ -5,165 +5,179 @@ Type=StaticCode Version=10.3 @EndOfDesignText@ ' ######################################## -' ##### HISTORIAL DE CAMBIOS ##### +' ##### HISTORIAL DE CAMBIOS ##### ' ######################################## + Sub Process_Globals - '- VERSION X.XX.XX (cabios a implementar) + + '- VERSION X.XX.XX (cambios a implementar) '- Agregar que se puedan usar cualquier cantidad de archivos config.properties '- Agregar que se pueda recargar solo un archivo de configuracion o todos a la vez. '- Agregar que el "Test" del manager revise (con el query de Jorge) cuantas conexiones hay actualmente activas, - ' o si no en el test, un nuevo handler, talvez "Conexiones". + '- o si no en el test, un nuevo handler, talvez "Conexiones". '- Agregar una forma de probar con carga el servidor '- Agregar la opcion de "Queries lentos" - - ' VERSION 5.09.14 + + '- VERSION 5.09.15 + ' - feat: Consolidación de mejoras en monitoreo, gestión de pools y hot-swap de configuración. + ' + ' Este commit integra y consolida todas las mejoras recientes en la robustez, + ' monitoreo de rendimiento y flexibilidad del servidor jRDC2-Multi. + ' + ' **Módulos Afectados:** Main.bas, RDCConnector.bas, DBHandlerB4X.bas, DBHandlerJSON.bas, Manager.bas, GlobalParameters.bas. + ' + ' **Cambios Clave Implementados:** + ' + ' 1. **Monitoreo Preciso y Robusto del Pool de Conexiones (C3P0) y Peticiones Activas:** + ' * Se corrigieron las métricas de `BusyConnections` y `TotalConnections` en `query_logs` y el panel `Manager` para reflejar el estado real del pool de C3P0. Esto se logró capturando `BusyConnections` directamente del pool *inmediatamente después* de que un handler adquiere una conexión en `DBHandlerJSON.bas` y `DBHandlerB4X.bas` [1, 2]. + ' * El contador de peticiones activas por base de datos (`GlobalParameters.ActiveRequestsCountByDB`) ahora se incrementa y decrementa de forma consistente. Se asegura que la `dbKey` se resuelva *antes* de la operación de conteo y se aplica una coerción explícita a `Int` (`.As(Int)`) para todas las operaciones, resolviendo inconsistencias de tipo [3, 4]. + ' * La lógica de decremento en la subrutina `Private Sub CleanupAndLog` (presente en `DBHandlerJSON.bas` y `DBHandlerB4X.bas`) se hizo más robusta, verificando que el contador sea mayor que cero antes de decrementar para evitar valores negativos [5]. + ' * Beneficio: Monitoreo preciso y fiable en tiempo real del uso del pool de conexiones y la carga de peticiones activas, mejorando el diagnóstico y la estabilidad del servidor [5, 6]. + ' + ' 2. **Implementación Completa de "Hot-Swap" para Recarga de Configuraciones de DB:** + ' * La lógica del comando `reload` en `Manager.bas` fue completamente rediseñada para permitir la recarga dinámica de configuraciones de bases de datos sin reiniciar el servidor [7, 8]. + ' * Se utiliza una instancia de `java.util.concurrent.locks.ReentrantLock` (`MainConnectorsLock`) declarada e inicializada en `Main.bas` para proteger de forma atómica la lectura y reemplazo del mapa `Main.Connectors`, que es compartido por múltiples hilos [9, 10]. + ' * Se añadió un método `Public Sub Close` en `RDCConnector.bas` [8], que utiliza `JavaObject` para invocar el método `close()` del `ConnectionPool` (C3P0) subyacente. Esto permite un cierre ordenado de los pools de conexión antiguos, liberando sus recursos de la base de datos de manera limpia durante el "hot-swap" [11, 12]. + ' * La implementación en `Manager.bas` incluye un manejo seguro del bloqueo sin `Finally` (usando una bandera booleana `lockAcquired`) y lógica de validación para abortar la recarga si ocurren errores críticos, manteniendo los conectores antiguos activos para evitar interrupciones del servicio [12, 13]. + ' * Beneficio: Capacidad crítica para actualizar configuraciones de conexión a bases de datos en caliente, mejorando la disponibilidad, simplificando el mantenimiento y previniendo fugas de recursos [14]. + ' + ' 3. **Manejo Mejorado de Peticiones POST con JSON en el Cuerpo:** + ' * `DBHandlerJSON.bas` fue modificado para detectar y procesar correctamente las peticiones POST que envían el payload JSON directamente en el cuerpo (con `Content-Type: application/json`), en lugar de solo en el parámetro `j` de la URL [14, 15]. + ' * Se asegura la lectura completa del `InputStream` y su cierre explícito para liberar recursos [15, 16]. + ' * Beneficio: Compatibilidad con estándares API web modernos, mejorando la robustez y la adherencia a los estándares sin comprometer la retrocompatibilidad con el "Método Legacy" (GET con parámetro `j`) [16]. + ' + ' 4. **Robustecimiento de la Inicialización del Pool de Conexiones (C3P0):** + ' * La subrutina `Initialize` en `RDCConnector.bas` fue reordenada y fortalecida. Ahora asegura que la configuración de C3P0 se cargue completamente en la variable de clase `config` y que todas las propiedades del pool se apliquen mediante `jo.RunMethod` *inmediatamente después* de `pool.Initialize` y *antes* de que el pool intente adquirir conexiones [17, 18]. + ' * Se añadieron las líneas `jo.RunMethod("setAcquireRetryAttempts", Array As Object(1))` y `jo.RunMethod("setBreakAfterAcquireFailure", Array As Object(True))` en `RDCConnector.bas`. Estas son cruciales para forzar a C3P0 a lanzar una `SQLException` explícita si falla al crear las conexiones iniciales, en lugar de fallar silenciosamente [18, 19]. + ' * Se implementó una "activación forzada" del pool (`Dim tempCon As SQL = pool.GetConnection` seguido de `tempCon.Close`) dentro de un bloque `Try...Catch` en `RDCConnector.Initialize`. Esto obliga al pool a establecer las conexiones iniciales (`InitialPoolSize`) con la configuración ya aplicada, permitiendo la captura de errores reales si la conexión a la base de datos falla [20]. + ' * Beneficio: Diagnóstico temprano y preciso de problemas de conexión a la base de datos, evitando situaciones donde `TotalConnections` mostraba `0` o la `jdbcUrl` aparecía truncada [19, 21]. + ' + ' 5. **Nuevas Funcionalidades en el Panel de Administración (Manager):** + ' * Se añadió el comando `slowqueries` al `Manager` para permitir la visualización de las 20 consultas más lentas registradas en la tabla `query_logs` de SQLite [22]. + ' * Se mejoró el comando `totalcon` en `Manager.bas` para mostrar estadísticas detalladas de *todos* los pools de conexión C3P0 configurados, obteniendo métricas en tiempo real (TotalConnections, BusyConnections, IdleConnections, etc.) de cada `RDCConnector` [2, 22]. + ' * Beneficio: Mayor visibilidad y control proactivo sobre el rendimiento y el uso de recursos del servidor desde la interfaz de administración. + ' + ' 6. **Optimización de la Gestión de Logs (`query_logs`):** + ' * Se implementó un `Public timerLogs As Timer` en `Main.bas` [conversación], que se inicializa en `AppStart` y ejecuta periódicamente (cada 10 minutos) la subrutina `borraArribaDe15000Logs`. + ' * La subrutina `borraArribaDe15000Logs` recorta la tabla `query_logs` en `users.db` para mantener solo los 15,000 registros más recientes, y luego realiza un `vacuum` para optimizar el espacio en disco utilizado por la base de datos SQLite [conversación]. + ' * Beneficio: Prevención del crecimiento excesivo de la base de datos de logs de rendimiento, manteniendo un historial manejable y optimizando el uso del almacenamiento a largo plazo. + + '- VERSION 5.09.14 (Ahora consolidado en 5.09.15) ' -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. - + ' -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. -' * **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 (Ahora consolidado en 5.09.15) '- 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'. - '- **Problemas Resueltos:** - '- 1. **Falta de "Hot-Swap" en `reload`:** El comando `reload` en `Manager.bas` no permitía la recarga dinámica de las configuraciones de la base de datos (config.properties) sin necesidad de reiniciar el servidor. La implementación anterior simplemente re-inicializaba las instancias existentes de `RDCConnector` in-situ, sin liberar los recursos de los pools de conexión anteriores, lo cual era ineficiente y propenso a errores. '- 2. **Ausencia de un mecanismo de cierre de pools:** No existía un método `Close` en `RDCConnector.bas` que permitiera cerrar ordenadamente los `ConnectionPool` (C3P0) y liberar las conexiones a la base de datos, lo que era crítico para un "hot-swap" limpio . '- 3. **Incompatibilidad con `Sync`:** La palabra clave `Sync` de B4X no era reconocida por el entorno de desarrollo del usuario, impidiendo su uso para la sincronización de hilos necesaria en el "hot-swap". '- 4. **Ausencia de `Finally` en B4X:** La palabra clave `Finally` (común en otros lenguajes como Java para asegurar la liberación de recursos) no está disponible directamente en B4X, lo cual planteó un desafío para garantizar la liberación del `ReentrantLock` de forma segura. - '- **Cambios Implementados:** - '- **En `Main.bas`:** '- * **Declaración de `MainConnectorsLock`:** Se añadió `Public MainConnectorsLock As JavaObject` en `Sub Process_Globals` para declarar una instancia de `java.util.concurrent.locks.ReentrantLock`, que servirá como objeto de bloqueo global para proteger el mapa `Main.Connectors`. '- * **Inicialización de `MainConnectorsLock`:** Se inicializó `MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null)` en `Sub AppStart`, asegurando que el objeto de bloqueo esté listo al inicio del servidor. - '- **En `RDCConnector.bas`:** '- * **Método `Public Sub Close`:** Se añadió esta subrutina al final del módulo. Utiliza `JavaObject` para invocar `joPool.RunMethod("close", Null)` sobre la instancia subyacente de C3P0, permitiendo un cierre ordenado y la liberación de todas las conexiones del pool . - '- **En `Manager.bas`:** '- * **Reemplazo completo de la lógica `If Command = "reload" Then`:** - '- * **Creación de `newConnectors`:** Se crea un mapa temporal (`Dim newConnectors As Map`) para inicializar las **nuevas instancias** de `RDCConnector` con la configuración fresca de los archivos `.properties` . - '- * **Preservación de `oldConnectors`:** Se almacena una referencia al mapa `Main.Connectors` actual en un nuevo mapa (`Dim oldConnectors As Map`) para tener acceso a los conectores antiguos que necesitan ser cerrados . - '- * **Sincronización con `ReentrantLock`:** Para proteger la manipulación del mapa `Main.Connectors` (que es compartido por múltiples hilos), se utilizan `Main.MainConnectorsLock.RunMethod("lock", Null)` y `Main.MainConnectorsLock.RunMethod("unlock", Null)`. Esto asegura que el reemplazo del mapa sea atómico, es decir, que solo un hilo pueda acceder a `Main.Connectors` durante la lectura y la escritura . - '- * **Manejo de Bloqueo Seguro sin `Finally`:** Dado que `Finally` no está disponible en B4X, se implementó un patrón con una bandera booleana (`lockAcquired`) dentro de un bloque `Try...Catch` para garantizar que `unlock()` siempre se ejecute si `lock()` fue exitoso, previniendo interbloqueos . - '- * **Cierre explícito de `oldConnectors`:** Después de que los `newConnectors` reemplazan a los `oldConnectors`, se itera sobre el mapa `oldConnectors` y se llama a `oldRDC.Close` para cada conector, liberando sus recursos de base de datos de manera limpia . - '- * **Validación de inicialización y control de errores:** Se agregó lógica para verificar el éxito de la inicialización de los nuevos conectores y abortar el "hot-swap" si ocurre un error crítico, manteniendo los conectores antiguos activos para evitar una interrupción del servicio . - '- * **Registro detallado:** Se mejoró la salida del log HTML del `Manager` para mostrar el proceso de recarga, las estadísticas de los pools recién inicializados y el cierre de los antiguos, incluyendo JSON detallado de las métricas de C3P0 . - + '- * **Creación de `newConnectors`:** Se crea un mapa temporal (`Dim newConnectors As Map`) para inicializar las **nuevas instancias** de `RDCConnector` con la configuración fresca de los archivos `.properties` . + '- * **Preservación de `oldConnectors`:** Se almacena una referencia al mapa `Main.Connectors` actual en un nuevo mapa (`Dim oldConnectors As Map`) para tener acceso a los conectores antiguos que necesitan ser cerrados . + '- * **Sincronización con `ReentrantLock`:** Para proteger la manipulación del mapa `Main.Connectors` (que es compartido por múltiples hilos), se utilizan `Main.MainConnectorsLock.RunMethod("lock", Null)` y `Main.MainConnectorsLock.RunMethod("unlock", Null)`. Esto asegura que el reemplazo del mapa sea atómico, es decir, que solo un hilo pueda acceder a `Main.Connectors` durante la lectura y la escritura . + '- * **Manejo de Bloqueo Seguro sin `Finally`:** Dado que `Finally` no está disponible en B4X, se implementó un patrón con una bandera booleana (`lockAcquired`) dentro de un bloque `Try...Catch` para garantizar que `unlock()` siempre se ejecute si `lock()` fue exitoso, previniendo interbloqueos . + '- * **Cierre explícito de `oldConnectors`:** Después de que los `newConnectors` reemplazan a los `oldConnectors`, se itera sobre el mapa `oldConnectors` y se llama a `oldRDC.Close` para cada conector, liberando sus recursos de base de datos de manera limpia . + '- * **Validación de inicialización y control de errores:** Se agregó lógica para verificar el éxito de la inicialización de los nuevos conectores y abortar el "hot-swap" si ocurre un error crítico, manteniendo los conectores antiguos activos para evitar una interrupción del servicio . + '- * **Registro detallado:** Se mejoró la salida del log HTML del `Manager` para mostrar el proceso de recarga, las estadísticas de los pools recién inicializados y el cierre de los antiguos, incluyendo JSON detallado de las métricas de C3P0 . '- • Beneficio: Estos cambios dotan al servidor jRDC2-Multi de una capacidad crítica para actualizar sus configuraciones de conexión a bases de datos en caliente, sin necesidad de reiniciar el servicio. Esto mejora la disponibilidad, simplifica el mantenimiento y previene fugas de recursos al asegurar el cierre ordenado de los pools de conexión antiguos. - '- VERSION 5.09.13.2 - '- Módulo: DBHandlerJSON.bas - '- Descripción de Cambios: Manejo de Peticiones POST con Content-Type: application/json - '- • Problema Identificado: La implementación anterior de DBHandlerJSON procesaba las peticiones POST esperando que el payload JSON se encontrara en el parámetro j de la URL (req.GetParameter("j")). Esto impedía la correcta lectura de peticiones POST que utilizaban Content-Type: application/json, donde el JSON se envía directamente en el cuerpo de la petición (InputStream). Como resultado, los clientes recibían un error indicando la ausencia del parámetro j . - '- • Solución Implementada: - '- 1. Se modificó la lógica en el método Handle para detectar explícitamente las peticiones POST con Content-Type igual a application/json. - '- 2. En estos casos, el payload JSON ahora se lee directamente del InputStream de la petición (req.InputStream). - '- 3. Se utilizó Bit.InputStreamToBytes(Is0) para leer el cuerpo completo de la petición a un Array de bytes, seguido de BytesToString para convertirlo en la cadena JSON. - '- 4. Se añadió el cierre explícito del InputStream (Is0.Close) para asegurar la liberación de recursos . - '- 5. Se corrigió el nombre de la variable Is a Is0 para evitar un conflicto con la palabra reservada Is de B4X . - '- 6. Se actualizó el mensaje de error para aclarar que el JSON puede faltar tanto en el parámetro j como en el cuerpo de la petición. - '- • Beneficio: Esta corrección asegura que el DBHandlerJSON sea compatible con el "Método Recomendado" de POST con application/json, mejorando la robustez y la adherencia a los estándares de las APIs web, Sin comprometer la retrocompatibilidad con el "Método Legacy" (GET con parámetro j). - - '- VERSION 5.09.13 -' feat: Mejora la inicialización del pool de conexiones y el soporte multi-DB. -' -' - Este commit aborda y resuelve varios problemas críticos relacionados con la inicialización -' del pool de conexiones (C3P0) para múltiples bases de datos y la depuración de logs -' en el servidor jRDC2-Multi. -' -' **Problemas Resueltos:** -' -' 1. **Inicialización de `TotalConnections: 0` en todos los pools:** Anteriormente, el Log mostraba 0 conexiones inicializadas para todas las bases de datos (DB1, DB2, DB3, DB4) durante `AppStart`, a pesar de que los `handlers` de `DBHandlerB4X` y `DBHandlerJSON` podían conectarse más tarde bajo demanda. Esto indicaba un fallo silencioso en la creación de conexiones iniciales por parte de C3P0. -' 2. **Configuración inconsistente de C3P0:** Parámetros críticos de C3P0 como `acquireRetryAttempts` y `breakAfterAcquireFailure` no se aplicaban correctamente al inicio, manteniendo los valores por defecto que ocultaban errores de conexión. -' 3. **`jdbcUrl` truncado/vacío:** Se observó que la `jdbcUrl` aparecía truncada o vacía en algunos logs de C3P0, indicando un problema en la carga de la configuración. -' -' **Cambios Implementados:** -' -' **En `Main.bas`:** -' -' * **Declaración de conectores:** Se aseguró la declaración de variables `Dim conX As RDCConnector` separadas para cada conector (con1, con2, con3, con4) para evitar conflictos de variables y asegurar la inicialización correcta. - -' **En `RDCConnector.bas`:** -' -' * **Corrección de *shadowing* de `config`:** Se modificó `LoadConfigMap(DB)` para asignar directamente a la variable de clase `config` (eliminando `Dim` local), resolviendo el problema de la `jdbcUrl` truncada y asegurando que cada `RDCConnector` use su configuración específica de manera persistente. -' * **Reordenamiento y robustecimiento de `Initialize`:** -' * **Carga de `config`:** Se asegura que `config` se cargue completamente en la variable de clase antes de cualquier operación del pool. -' * **Configuración de C3P0:** Todas las propiedades del pool (incluyendo `setInitialPoolSize`, `setMinPoolSize`, `setMaxPoolSize`, `setMaxIdleTime`, etc. ahora se aplican mediante `jo.RunMethod` *inmediatamente después* de `pool.Initialize` y *antes* de que el pool intente adquirir conexiones. -' * **Forzar reportes de errores:** Se añadieron las líneas `jo.RunMethod("setAcquireRetryAttempts", Array As Object(1))` y `jo.RunMethod("setBreakAfterAcquireFailure", Array As Object(True))`. Estas son cruciales para forzar a C3P0 a lanzar una `SQLException` explícita si falla al crear las conexiones iniciales, en lugar de fallar silenciosamente. -' * **Activación forzada del pool:** Se implementó `Dim tempCon As SQL = pool.GetConnection` seguido de `tempCon.Close` dentro de un bloque `Try...Catch`. Esto obliga al pool a establecer las conexiones iniciales (`InitialPoolSize`) con la configuración ya aplicada, permitiendo la captura de errores reales si la conexión falla. + '- VERSION 5.09.13.2 (Ahora consolidado en 5.09.15) + '- Módulo: DBHandlerJSON.bas + '- Descripción de Cambios: Manejo de Peticiones POST con Content-Type: application/json + '- • Problema Identificado: La implementación anterior de DBHandlerJSON procesaba las peticiones POST esperando que el payload JSON se encontrara en el parámetro j de la URL (req.GetParameter("j")). Esto impedía la correcta lectura de peticiones POST que utilizaban Content-Type: application/json, donde el JSON se envía directamente en el cuerpo de la petición (InputStream). Como resultado, los clientes recibían un error indicando la ausencia del parámetro j . + '- • Solución Implementada: + '- 1. Se modificó la lógica en el método Handle para detectar explícitamente las peticiones POST con Content-Type igual a application/json. + '- 2. En estos casos, el payload JSON ahora se lee directamente del InputStream de la petición (req.InputStream). + '- 3. Se utilizó Bit.InputStreamToBytes(Is0) para leer el cuerpo completo de la petición a un Array de bytes, seguido de BytesToString para convertirlo en la cadena JSON. + '- 4. Se añadió el cierre explícito del InputStream (Is0.Close) para asegurar la liberación de recursos . + '- 5. Se corrigió el nombre de la variable Is a Is0 para evitar un conflicto con la palabra reservada Is de B4X . + '- 6. Se actualizó el mensaje de error para aclarar que el JSON puede faltar tanto en el parámetro j como en el cuerpo de la petición. + '- • Beneficio: Esta corrección asegura que el DBHandlerJSON sea compatible con el "Método Recomendado" de POST con application/json, mejorando la robustez y la adherencia a los estándares de las APIs web, Sin comprometer la retrocompatibilidad con el "Método Legacy" (GET con parámetro j). + + '- VERSION 5.09.13 (Ahora consolidado en 5.09.15) + ' feat: Mejora la inicialización del pool de conexiones y el soporte multi-DB. + ' + ' **Problemas Resueltos:** + ' + ' 1. **Inicialización de `TotalConnections: 0` en todos los pools:** Anteriormente, el Log mostraba 0 conexiones inicializadas para todas las bases de datos (DB1, DB2, DB3, DB4) durante `AppStart`, a pesar de que los `handlers` de `DBHandlerB4X` y `DBHandlerJSON` podían conectarse más tarde bajo demanda. Esto indicaba un fallo silencioso en la creación de conexiones iniciales por parte de C3P0. + ' 2. **Configuración inconsistente de C3P0:** Parámetros críticos de C3P0 como `acquireRetryAttempts` y `breakAfterAcquireFailure` no se aplicaban correctamente al inicio, manteniendo los valores por defecto que ocultaban errores de conexión. + ' 3. **`jdbcUrl` truncado/vacío:** Se observó que la `jdbcUrl` aparecía truncada o vacía en algunos logs de C3P0, indicando un problema en la carga de la configuración. + ' + ' **Cambios Implementados:** + ' + ' **En `Main.bas`:** + ' + ' * **Declaración de conectores:** Se aseguró la declaración de variables `Dim conX As RDCConnector` separadas para cada conector (con1, con2, con3, con4) para evitar conflictos de variables y asegurar la inicialización correcta. + ' + ' **En `RDCConnector.bas`:** + ' + ' * **Corrección de shadowing de `config`:** Se modificó `LoadConfigMap(DB)` para asignar directamente a la variable de clase `config` (eliminando `Dim` local), resolviendo el problema de la `jdbcUrl` truncada y asegurando que cada `RDCConnector` use su configuración específica de manera persistente. + ' * **Reordenamiento y robustecimiento de `Initialize`:** + ' * **Carga de `config`:** Se asegura que `config` se cargue completamente en la variable de clase antes de cualquier operación del pool. + ' * **Configuración de C3P0:** Todas las propiedades del pool (incluyendo `setInitialPoolSize`, `setMinPoolSize`, `setMaxPoolSize`, `setMaxIdleTime`, etc. ahora se aplican mediante `jo.RunMethod` *inmediatamente después* de `pool.Initialize` y *antes* de que el pool intente adquirir conexiones. + ' * **Forzar reportes de errores:** Se añadieron las líneas `jo.RunMethod("setAcquireRetryAttempts", Array As Object(1))` y `jo.RunMethod("setBreakAfterAcquireFailure", Array As Object(True))`. Estas son cruciales para forzar a C3P0 a lanzar una `SQLException` explícita si falla al crear las conexiones iniciales, en lugar de fallar silenciosamente. + ' * **Activación forzada del pool:** Se implementó `Dim tempCon As SQL = pool.GetConnection` seguido de `tempCon.Close` dentro de un bloque `Try...Catch`. Esto obliga al pool a establecer las conexiones iniciales (`InitialPoolSize`) con la configuración ya aplicada, permitiendo la captura de errores reales si la conexión falla. - '- VERSION 5.09.08 '- Se agregó que se puedan configurar en el config.properties los siguientes parametros: - ' - ' - setInitialPoolSize = 3 - ' - setMinPoolSize = 2 - ' - setMaxPoolSize = 5 - ' + '- - 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. - ' + '- - 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. '- Se agregó en el config.properties, al final del "JdbcUrl" este parametro, que le indica al servidor de Oracle - ' el nombre del cliente que se está conectando "?v$session.program=jRDC_Multi" - + '- el nombre del cliente que se está conectando "?v$session.program=jRDC_Multi" '- VERSION 5.09.08 '- Se cambio el codigo para que en lugar de esperar un mapa con los parametros del query y nombres de los parametros (par1, par2, etc) para definir el ordenamiento, ahora se espera una lista [1,"2",3], y el orden de los parametros se toma directamente del orden en el que se mandan, de la misma forma que en B4A. - '- VERSION 5.09.04 '- Se cambio el nombre del handler de B4X a DBHandlerB4X. '- Se quitaron los handlers que ya no servian. - '- VERSION 5.09.01 '- Se corrigieron errores en "Manager". '- Se cambiaron nombres de handlers. '- Se corrigio un error en la ruta de "www/login.html". - '- VERSION 5.08.31 '- Se corrigio que no avisaba cuando el query no requeria parametros y si se enviaban (en el JSONHandler) - '- VERSION 5.08.30 '- Se cambiaron los 4 handlers de B4A a uno solo que toma el DB de la ruta automáticamente. '- Se agregaron validaciones del numero de parametros y si el query no los requiere o se dan de mas o de menos, manda un error especificando eso, ya no se reciben errores directos de la base de datos, esto fue tanto para B4A como para JSON. '- Se modificó el Readme.md para incluir todos estos cambios. - '- VERSION 5.08.25 '- Se modificaron los archivos de reinicio de los servicios (servidor y Bow) y se cambio el menu del "manager" para que a seccion de "reload" incluya la liga a reinciar Bow. - '- VERSION 5.08.02 '- Se hizo un cambio para tratar de que las conexiones se "identifiquen" con Oracle y Jorge pueda saber que conexiones/recursos estamos ocupando - '- VERSION 4.11.14 '- Se agregó el parametro "setMaxPoolSize=5" para que solo genere 5 conexiones a la base de datos, antes generaba 15. '- Se quitaron lineas previamente comentadas. - '- VERSION 4.11.09 '- Commit inicial on Nov 9, 2024 -End Sub + +End Sub \ No newline at end of file diff --git a/ChangePassHandler.bas b/ChangePassHandler.bas index 3be2adb..2ad28e0 100644 --- a/ChangePassHandler.bas +++ b/ChangePassHandler.bas @@ -10,7 +10,7 @@ Sub Class_Globals End Sub Public Sub Initialize -' bc.Initialize ' <<--- CORRECCIÓN 1: Descomentado para que el objeto se cree. + bc.Initialize("BC") End Sub Public Sub Handle(req As ServletRequest, resp As ServletResponse) diff --git a/DBHandlerB4X.bas b/DBHandlerB4X.bas index ad897ab..4f0bacb 100644 --- a/DBHandlerB4X.bas +++ b/DBHandlerB4X.bas @@ -4,178 +4,230 @@ ModulesStructureVersion=1 Type=Class Version=10.3 @EndOfDesignText@ -' Handler genérico para peticiones desde clientes B4A/B4i (DBRequestManager) -' Determina la base de datos a utilizar dinámicamente a partir de la URL de la petición. -' Versión con validación de parámetros y errores en texto plano. -Sub Class_Globals - ' Estas constantes y variables solo se compilan si se usa la #if VERSION1, - ' lo que sugiere que es para dar soporte a una versión antigua del protocolo de comunicación. -' #if VERSION1 - ' Constantes para identificar los tipos de datos en la serialización personalizada (protocolo V1). - Private const T_NULL = 0, T_STRING = 1, T_SHORT = 2, T_INT = 3, T_LONG = 4, T_FLOAT = 5 _ - ,T_DOUBLE = 6, T_BOOLEAN = 7, T_BLOB = 8 As Byte - ' Utilidades para convertir entre tipos de datos y arrays de bytes. - Private bc As ByteConverter - ' Utilidad para comprimir/descomprimir streams de datos (usado en V1). - Private cs As CompressedStreams -' #end if +' Módulo de clase: DBHandlerB4X +' Este handler genérico se encarga de procesar las peticiones HTTP provenientes +' de clientes B4A/B4i (que utilizan la librería DBRequestManager). +' La base de datos a utilizar (DB1, DB2, etc.) se determina dinámicamente +' a partir de la URL de la petición. +' Esta versión incluye validaciones de parámetros y manejo de errores. - ' Mapa para convertir tipos de columna JDBC de fecha/hora a métodos de obtención de datos. +Sub Class_Globals + ' --- Variables globales de la clase --- + + ' La siguiente sección de constantes y utilidades se compila condicionalmente + ' solo si la directiva #if VERSION1 está activa. Esto es para dar soporte + ' a una versión antigua del protocolo de comunicación de DBRequestManager. + #if VERSION1 + ' Constantes para identificar los tipos de datos en la serialización personalizada (protocolo V1). + Private const T_NULL = 0, T_STRING = 1, T_SHORT = 2, T_INT = 3, T_LONG = 4, T_FLOAT = 5 _ + ,T_DOUBLE = 6, T_BOOLEAN = 7, T_BLOB = 8 As Byte + ' Utilidades para convertir entre tipos de datos y arrays de bytes. + Private bc As ByteConverter + ' Utilidad para comprimir/descomprimir streams de datos (usado en V1). + Private cs As CompressedStreams + #end if + + ' Mapa para convertir tipos de columna JDBC de fecha/hora a los nombres de métodos de Java + ' para obtener los valores correctos de ResultSet. Private DateTimeMethods As Map - ' Objeto que gestiona las conexiones a las diferentes bases de datos definidas en config.properties. + + ' Objeto que gestiona las conexiones al pool de una base de datos específica. + ' Esta instancia de RDCConnector será asignada en el método Handle según la dbKey de la petición. Private Connector As RDCConnector End Sub -' Se ejecuta una vez cuando se crea una instancia de esta clase. +' Se ejecuta una vez cuando se crea una instancia de esta clase por el servidor HTTP. Public Sub Initialize ' Inicializa el mapa que asocia los códigos de tipo de columna de fecha/hora de JDBC - ' con los nombres de los métodos correspondientes para leerlos correctamente. + ' con los nombres de los métodos correspondientes para leerlos correctamente desde un ResultSet. DateTimeMethods = CreateMap(91: "getDate", 92: "getTime", 93: "getTimestamp") 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 handler. +' req: El objeto ServletRequest que contiene la información de la petición entrante. +' resp: El objeto ServletResponse para construir y enviar la respuesta al cliente. Sub Handle(req As ServletRequest, resp As ServletResponse) - ' === INICIO DE LA LÓGICA DINÁMICA (Extracción de dbKey de la URL) === + ' === INICIO DE LA LÓGICA DINÁMICA: Extracción de dbKey de la URL === + ' Esta sección analiza la URL de la petición para determinar a qué base de datos + ' (DB1, DB2, etc.) se dirige la solicitud. Por ejemplo, si la URL es "/DB2/query", + ' el 'dbKey' extraído será "DB2". Dim URI As String = req.RequestURI - Dim dbKey As String ' Usamos dbKey para consistencia con tu código original. + Dim dbKey As String ' Variable para almacenar el identificador de la base de datos. + If URI.Length > 1 And URI.StartsWith("/") Then - dbKey = URI.Substring(1) '[DBHandlerB4X.bas.txt, 51] + dbKey = URI.Substring(1) ' Elimina el '/' inicial. If dbKey.Contains("/") Then - dbKey = dbKey.SubString2(0, dbKey.IndexOf("/")) '[DBHandlerB4X.bas.txt, 51] + ' Si la URL tiene más segmentos (ej. "/DB2/alguna_ruta"), toma solo el primer segmento como dbKey. + dbKey = dbKey.SubString2(0, dbKey.IndexOf("/")) End If Else - dbKey = "DB1" '[DBHandlerB4X.bas.txt, 51] + ' Si la URL es solo "/", por defecto se usa "DB1". + dbKey = "DB1" End If - dbKey = dbKey.ToUpperCase '[DBHandlerB4X.bas.txt, 52] + dbKey = dbKey.ToUpperCase ' Normaliza el dbKey a mayúsculas para consistencia. - If Main.Connectors.ContainsKey(dbKey) = False Then '[DBHandlerB4X.bas.txt, 52] - Dim ErrorMsg As String = $"Invalid DB key specified in URL: '${dbKey}'. Valid keys are: ${Main.listaDeCP}"$ '[DBHandlerB4X.bas.txt, 52] - Log(ErrorMsg) '[DBHandlerB4X.bas.txt, 52] - SendPlainTextError(resp, 400, ErrorMsg) '[DBHandlerB4X.bas.txt, 52] - ' Aquí no se necesita CleanupAndLog, ya que el contador no se ha incrementado - ' y no se ha obtenido ninguna conexión del pool aún. - Return + ' Verifica si el dbKey extraído corresponde a una base de datos configurada y cargada en Main. + If Main.Connectors.ContainsKey(dbKey) = False Then + ' Si la base de datos no es válida, se construye un mensaje de error y se envía. + Dim ErrorMsg As String = $"Invalid DB key specified in URL: '${dbKey}'. Valid keys are: ${Main.listaDeCP}"$ + Log(ErrorMsg) + SendPlainTextError(resp, 400, ErrorMsg) ' Envía una respuesta de error al cliente. + ' No se llama a CleanupAndLog aquí, ya que el contador de peticiones no se ha incrementado + ' y no se ha obtenido ninguna conexión del pool. + Return ' Termina la ejecución del handler. End If ' === FIN DE LA LÓGICA DINÁMICA === - Log("********************* " & dbKey & " ********************") '[DBHandlerB4X.bas.txt, 53] + Log("********************* " & dbKey & " ********************") ' Log de depuración para identificar la base de datos. - Dim start As Long = DateTime.Now '[___new 3.txt, 203] + Dim start As Long = DateTime.Now ' Registra el tiempo de inicio de la petición para calcular la duración. ' --- INICIO: Conteo de peticiones activas para esta dbKey (Incrementar) --- - Dim currentActiveRequests As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0) '[___new 3.txt, 205] - GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentActiveRequests + 1) '[___new 3.txt, 205] - Dim requestsBeforeDecrement As Int = currentActiveRequests + 1 '[___new 3.txt, 207] + ' 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. + ' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor inicial sea un Int y lo recuperamos como Int! >>>> + Dim currentActiveRequests As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0).As(Int) + GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentActiveRequests + 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'. + Dim requestsBeforeDecrement As Int = currentActiveRequests + 1 + ' Log($"[DEBUG] Handle Increment (B4X): dbKey=${dbKey}, currentCountFromMap=${currentActiveRequests}, requestsBeforeDecrement=${requestsBeforeDecrement}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) ' --- FIN: Conteo de peticiones activas --- - ' Declaraciones de variables con alcance en toda la subrutina para la limpieza. + ' Declaraciones de variables con alcance en toda la subrutina para asegurar la limpieza final. Dim q As String = "unknown_b4x_command" ' Nombre del comando para el log, con valor por defecto. Dim con As SQL ' La conexión a la BD, se inicializará más tarde. - Dim duration As Long ' La duración de la petición, calculada antes del log. + Dim 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. Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler --- - Dim in As InputStream = req.InputStream '[DBHandlerB4X.bas.txt, 53] - Dim method As String = req.GetParameter("method") '[DBHandlerB4X.bas.txt, 53] - Connector = Main.Connectors.Get(dbKey) '[DBHandlerB4X.bas.txt, 54] + Dim in As InputStream = req.InputStream ' Obtiene el stream de entrada de la petición HTTP. + Dim method As String = req.GetParameter("method") ' Obtiene el parámetro 'method' de la URL (ej. "query2", "batch2"). + Connector = Main.Connectors.Get(dbKey) ' Asigna la instancia de RDCConnector para esta dbKey. + + con = Connector.GetConnection(dbKey) ' ¡La conexión a la BD se obtiene aquí del pool de conexiones! - con = Connector.GetConnection(dbKey) ' La conexión a la BD se obtiene aquí. [DBHandlerB4X.bas.txt, 54] - ' <<<< ¡BUSY_CONNECTIONS YA SE CAPTURABA BIEN! >>>> + ' 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 '[___new 3.txt, 204] + Dim poolStats As Map = Connector.GetPoolStats If poolStats.ContainsKey("BusyConnections") Then - poolBusyConnectionsForLog = poolStats.Get("BusyConnections") ' Capturamos el valor. + ' <<<< ¡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! >>>> - Log("Metodo: " & method) '[DBHandlerB4X.bas.txt, 54] + Log("Metodo: " & method) ' Log de depuración para identificar el método de la petición. + ' --- Lógica para ejecutar diferentes tipos de comandos basados en el parámetro 'method' --- If method = "query2" Then - q = ExecuteQuery2(dbKey, con, in, resp) '[DBHandlerB4X.bas.txt, 54] + ' Ejecuta una consulta única utilizando el protocolo V2 (B4XSerializator). + 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 + Return ' Salida temprana si hay un error. End If - '#if VERSION1 - Else if method = "query" Then - in = cs.WrapInputStream(in, "gzip") - 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 - in = cs.WrapInputStream(in, "gzip") - 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 + #if VERSION1 + ' Estas ramas se compilan solo si #if VERSION1 está activo (para protocolo antiguo). + Else if method = "query" Then + in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1. + q = ExecuteQuery(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 if method = "batch" Then + in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1. + q = ExecuteBatch(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 + #end if Else if method = "batch2" Then - q = ExecuteBatch2(dbKey, con, in, resp) '[DBHandlerB4X.bas.txt, 55] + ' Ejecuta un lote de comandos (INSERT, UPDATE, DELETE) utilizando el protocolo V2. + 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 + Return ' Salida temprana si hay un error. End If Else - Log("Unknown method: " & method) '[DBHandlerB4X.bas.txt, 56] - SendPlainTextError(resp, 500, "unknown method") '[DBHandlerB4X.bas.txt, 56] - q = "unknown_method_handler" ' Aseguramos un valor para q en el log. + ' Si el método solicitado no es reconocido, se registra un error y se envía una respuesta adecuada. + Log("Unknown method: " & method) + SendPlainTextError(resp, 500, "unknown method") + q = "unknown_method_handler" ' Aseguramos un valor para 'q' para que el log sea informativo. duration = DateTime.Now - start CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) - Return + Return ' Salida temprana. End If Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL --- - Log(LastException) '[DBHandlerB4X.bas.txt, 56] - SendPlainTextError(resp, 500, LastException.Message) '[DBHandlerB4X.bas.txt, 56] - q = "error_in_b4x_handler" ' Aseguramos un valor para q en el log si hay excepción. + ' Si ocurre una excepción inesperada durante el procesamiento de la petición. + Log(LastException) ' Registra la excepción completa en el log. + SendPlainTextError(resp, 500, LastException.Message) ' Envía un error 500 al cliente. + q = "error_in_b4x_handler" ' Aseguramos un valor para 'q' en caso de excepción. End Try ' --- FIN: Bloque Try principal --- ' --- Lógica de logging y limpieza final (para rutas de ejecución normal o después de Catch) --- - duration = DateTime.Now - start '[DBHandlerB4X.bas.txt, 57] - Log($"Command: ${q}, took: ${duration}ms, client=${req.RemoteAddress}"$) '[DBHandlerB4X.bas.txt, 57] + ' 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. + Log($"Command: ${q}, took: ${duration}ms, client=${req.RemoteAddress}"$) ' Logea el comando y la duración. + ' Llama a la subrutina centralizada para registrar el rendimiento y limpiar recursos. CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) End Sub -' --- NUEVA SUBRUTINA: Centraliza el logging y la limpieza --- +' --- 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) - ' 1. Llama a la subrutina centralizada para registrar el rendimiento. - Main.LogQueryPerformance(qName, durMs, dbKey, clientIp, handlerReqs, poolBusyConns) '[___new 3.txt, 207] + ' Log($"[DEBUG] CleanupAndLog Entry (B4X): 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 (B4X): dbKey=${dbKey}, currentCount (as Int)=${currentCount}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) - ' <<<< ¡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 + ' Si el contador es positivo, lo decrementamos. GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1) Else - ' Si el contador ya está en 0 o negativo, registramos una advertencia y lo aseguramos en 0. + ' 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 (B4X): 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. + ' 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 para manejar la ejecución de queries y batches (Protocolo V2) --- + ' Ejecuta una consulta única usando el protocolo V2 (B4XSerializator). -Private Sub ExecuteQuery2 (DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String - ' Objeto para deserializar los datos enviados desde el cliente. - Dim ser As B4XSerializator +' DB: Identificador de la base de datos. +' con: La conexión SQL obtenida del pool. +' in: InputStream de la petición. +' resp: ServletResponse para enviar la respuesta. +' Retorna el nombre del comando ejecutado o "error" si falló. +Private Sub ExecuteQuery2 (DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String + Dim ser As B4XSerializator ' Objeto para deserializar los datos enviados desde el cliente. ' Convierte el stream de entrada a un array de bytes y luego a un objeto Mapa. Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in)) - ' Extrae el objeto DBCommand del mapa. + ' Extrae el objeto DBCommand (nombre de la query y sus parámetros) del mapa. Dim cmd As DBCommand = m.Get("command") - ' Extrae el límite de filas a devolver. + ' Extrae el límite de filas a devolver (para paginación). Dim limit As Int = m.Get("limit") ' Obtiene la sentencia SQL correspondiente al nombre del comando desde config.properties. @@ -200,7 +252,7 @@ Private Sub ExecuteQuery2 (DB As String, con As SQL, in As InputStream, resp As ' Cuenta cuántos parámetros se recibieron. Dim receivedParams As Int If cmd.Parameters = Null Then receivedParams = 0 Else receivedParams = cmd.Parameters.Length - + ' Compara el número de parámetros esperados con los recibidos. If expectedParams <> receivedParams Then Dim errorMessage As String = $"Número de parametros equivocado para "${cmd.Name}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ @@ -214,25 +266,30 @@ Private Sub ExecuteQuery2 (DB As String, con As SQL, in As InputStream, resp As ' Ejecuta la consulta SQL con los parámetros proporcionados. Dim rs As ResultSet = con.ExecQuery2(sqlCommand, cmd.Parameters) + ' Si el límite es 0 o negativo, lo establece a un valor muy alto (máximo entero). If limit <= 0 Then limit = 0x7fffffff 'max int + ' Obtiene el objeto Java subyacente del ResultSet para acceder a métodos adicionales. Dim jrs As JavaObject = rs ' Obtiene los metadatos del ResultSet (información sobre las columnas). Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) ' Obtiene el número de columnas del resultado. Dim cols As Int = rs.ColumnCount - ' Crea un objeto DBResult para empaquetar la respuesta. - Dim res As DBResult + + Dim res As DBResult ' Crea un objeto DBResult para empaquetar la respuesta. res.Initialize res.columns.Initialize res.Tag = Null + ' Llena el mapa de columnas con el nombre de cada columna y su índice. For i = 0 To cols - 1 res.columns.Put(rs.GetColumnName(i), i) Next + ' Inicializa la lista de filas. res.Rows.Initialize + ' Itera sobre cada fila del ResultSet, hasta llegar al límite. Do While rs.NextRow And limit > 0 Dim row(cols) As Object @@ -267,27 +324,36 @@ Private Sub ExecuteQuery2 (DB As String, con As SQL, in As InputStream, resp As Loop ' Cierra el ResultSet para liberar recursos. rs.Close + ' Serializa el objeto DBResult completo a un array de bytes. Dim data() As Byte = ser.ConvertObjectToBytes(res) ' Escribe los datos serializados en el stream de respuesta. resp.OutputStream.WriteBytes(data, 0, data.Length) + ' Devuelve el nombre del comando para el log. Return "query: " & cmd.Name End Sub ' Ejecuta un lote de comandos (INSERT, UPDATE, DELETE) usando el protocolo V2. +' DB: Identificador de la base de datos. +' con: La conexión SQL obtenida del pool. +' in: InputStream de la petición. +' resp: ServletResponse para enviar la respuesta. +' Retorna un resumen del lote para el log, o "error" si falló. Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String Dim ser As B4XSerializator ' Deserializa el mapa que contiene la lista de comandos. Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in)) ' Obtiene la lista de objetos DBCommand. Dim commands As List = m.Get("commands") + ' Prepara un objeto DBResult para la respuesta (aunque para batch no devuelve datos, solo confirmación). Dim res As DBResult res.Initialize - res.columns = CreateMap("AffectedRows (N/A)": 0) + res.columns = CreateMap("AffectedRows (N/A)": 0) ' Columna simbólica. res.Rows.Initialize res.Tag = Null + Try ' Inicia una transacción. Todos los comandos del lote se ejecutarán como una unidad. con.BeginTransaction @@ -311,7 +377,6 @@ Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As S Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length Dim receivedParams As Int If cmd.Parameters = Null Then receivedParams = 0 Else receivedParams = cmd.Parameters.Length - ' Si el número de parámetros no coincide, deshace la transacción y envía error. If expectedParams <> receivedParams Then con.Rollback @@ -322,328 +387,330 @@ Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As S End If End If ' --- FIN VALIDACIÓN --- - - ' Ejecuta el comando (no es una consulta, no devuelve filas). - con.ExecNonQuery2(sqlCommand, cmd.Parameters) + + con.ExecNonQuery2(sqlCommand, cmd.Parameters) ' Ejecuta el comando (no es una consulta, no devuelve filas). Next - ' Añade una fila simbólica al resultado para indicar éxito. - res.Rows.Add(Array As Object(0)) - ' Si todos los comandos se ejecutaron sin error, confirma la transacción. - con.TransactionSuccessful + + res.Rows.Add(Array As Object(0)) ' Añade una fila simbólica al resultado para indicar éxito. + con.TransactionSuccessful ' Si todos los comandos se ejecutaron sin error, confirma la transacción. Catch ' Si cualquier comando falla, se captura el error. con.Rollback ' Se deshacen todos los cambios hechos en la transacción. - Log(LastException) - SendPlainTextError(resp, 500, LastException.Message) + Log(LastException) ' Registra la excepción. + SendPlainTextError(resp, 500, LastException.Message) ' Envía un error 500 al cliente. End Try + ' Serializa y envía la respuesta al cliente. Dim data() As Byte = ser.ConvertObjectToBytes(res) resp.OutputStream.WriteBytes(data, 0, data.Length) + ' Devuelve un resumen para el log. Return $"batch (size=${commands.Size})"$ End Sub -' Código compilado condicionalmente para el protocolo antiguo (V1). -'#if VERSION1 +' --- Subrutinas para manejar la ejecución de queries y batches (Protocolo V1 - Compilación Condicional) --- +' Este código se compila solo si #if VERSION1 está activo, para mantener compatibilidad con clientes antiguos. +#if VERSION1 ' Ejecuta un lote de comandos usando el protocolo V1. Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String - ' Lee y descarta la versión del cliente. - Dim clientVersion As Float = ReadObject(in) 'ignore - ' Lee cuántos comandos vienen en el lote. - Dim numberOfStatements As Int = ReadInt(in) - Dim res(numberOfStatements) As Int ' Array para resultados (aunque no se usa). - Try - con.BeginTransaction - ' Itera para procesar cada comando del lote. - For i = 0 To numberOfStatements - 1 - ' Lee el nombre del comando y la lista de parámetros usando el deserializador V1. - Dim queryName As String = ReadObject(in) - Dim params As List = ReadList(in) - - Dim sqlCommand As String = Connector.GetCommand(DB, queryName) + ' Lee y descarta la versión del cliente. + Dim clientVersion As Float = ReadObject(in) 'ignore + ' Lee cuántos comandos vienen en el lote. + Dim numberOfStatements As Int = ReadInt(in) + Dim res(numberOfStatements) As Int ' Array para resultados (aunque no se usa). - ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> - If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then - con.Rollback - Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ - Log(errorMessage) - SendPlainTextError(resp, 400, errorMessage) - Return "error" - End If - ' <<< FIN NUEVA VALIDACIÓN >>> - - ' --- INICIO VALIDACIÓN DE PARÁMETROS DENTRO DEL BATCH (V1) --- - If sqlCommand.Contains("?") Or (params <> Null And params.Size > 0) Then - Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length - Dim receivedParams As Int - If params = Null Then receivedParams = 0 Else receivedParams = params.Size + Try + con.BeginTransaction + ' Itera para procesar cada comando del lote. + For i = 0 To numberOfStatements - 1 + ' Lee el nombre del comando y la lista de parámetros usando el deserializador V1. + Dim queryName As String = ReadObject(in) + Dim params As List = ReadList(in) + Dim sqlCommand As String = Connector.GetCommand(DB, queryName) - If expectedParams <> receivedParams Then - con.Rollback - Dim errorMessage As String = $"Número de parametros equivocado para "${queryName}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ - Log(errorMessage) - SendPlainTextError(resp, 400, errorMessage) - Return "error" - End If - End If - ' --- FIN VALIDACIÓN --- - - ' Ejecuta el comando. - con.ExecNonQuery2(sqlCommand, params) - Next - ' Confirma la transacción. - con.TransactionSuccessful - - ' Comprime la salida antes de enviarla. - Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") - ' Escribe la respuesta usando el serializador V1. - WriteObject(Main.VERSION, out) - WriteObject("batch", out) - WriteInt(res.Length, out) - For Each r As Int In res - WriteInt(r, out) - Next - out.Close - Catch - con.Rollback - Log(LastException) - SendPlainTextError(resp, 500, LastException.Message) - End Try - Return $"batch (size=${numberOfStatements})"$ + ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> + If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then + con.Rollback ' Deshace la transacción si un comando es inválido. + Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ + Log(errorMessage) + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If + ' <<< FIN NUEVA VALIDACIÓN >>> + + ' --- INICIO VALIDACIÓN DE PARÁMETROS DENTRO DEL BATCH (V1) --- + If sqlCommand.Contains("?") Or (params <> Null And params.Size > 0) Then + Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length + Dim receivedParams As Int + If params = Null Then receivedParams = 0 Else receivedParams = params.Size + + If expectedParams <> receivedParams Then + con.Rollback + Dim errorMessage As String = $"Número de parametros equivocado para "${queryName}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ + Log(errorMessage) + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If + End If + ' --- FIN VALIDACIÓN --- + + con.ExecNonQuery2(sqlCommand, params) ' Ejecuta el comando. + Next + + con.TransactionSuccessful ' Confirma la transacción. + + Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime la salida antes de enviarla. + ' Escribe la respuesta usando el serializador V1. + WriteObject(Main.VERSION, out) + WriteObject("batch", out) + WriteInt(res.Length, out) + For Each r As Int In res + WriteInt(r, out) + Next + out.Close + + Catch + con.Rollback + Log(LastException) + SendPlainTextError(resp, 500, LastException.Message) + End Try + + Return $"batch (size=${numberOfStatements})"$ End Sub ' Ejecuta una consulta única usando el protocolo V1. -Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String - Log("====================== ExecuteQuery =====================") - ' Deserializa los datos de la petición usando el protocolo V1. - Dim clientVersion As Float = ReadObject(in) 'ignore - Dim queryName As String = ReadObject(in) - Dim limit As Int = ReadInt(in) - Dim params As List = ReadList(in) - ' Obtiene la sentencia SQL. - Dim theSql As String = Connector.GetCommand(DB, queryName) -' Log(444 & "|" & theSql) - - ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> - If theSql = Null Or theSql ="null" Or theSql.Trim = "" Then - Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ - Log(errorMessage) - SendPlainTextError(resp, 400, errorMessage) - Return "error" - End If - ' <<< FIN NUEVA VALIDACIÓN >>> +Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String + Log("====================== ExecuteQuery =====================") + ' Deserializa los datos de la petición usando el protocolo V1. + Dim clientVersion As Float = ReadObject(in) 'ignore + Dim queryName As String = ReadObject(in) + Dim limit As Int = ReadInt(in) + Dim params As List = ReadList(in) - ' --- INICIO VALIDACIÓN DE PARÁMETROS (V1) --- - If theSql.Contains("?") Or (params <> Null And params.Size > 0) Then - Dim expectedParams As Int = theSql.Length - theSql.Replace("?", "").Length - Dim receivedParams As Int - If params = Null Then receivedParams = 0 Else receivedParams = params.Size + ' Obtiene la sentencia SQL. + Dim theSql As String = Connector.GetCommand(DB, queryName) - If expectedParams <> receivedParams Then - Dim errorMessage As String = $"Número de parametros equivocado para "${queryName}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ - Log(errorMessage) - SendPlainTextError(resp, 400, errorMessage) - Return "error" - End If - End If - ' --- FIN VALIDACIÓN --- + ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> + If theSql = Null Or theSql ="null" Or theSql.Trim = "" Then + Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ + Log(errorMessage) + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If + ' <<< FIN NUEVA VALIDACIÓN >>> - ' Ejecuta la consulta. - Dim rs As ResultSet = con.ExecQuery2(theSql, params) - If limit <= 0 Then limit = 0x7fffffff 'max int - Dim jrs As JavaObject = rs - Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) - Dim cols As Int = rs.ColumnCount - ' Comprime el stream de salida. - Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") - ' Escribe la cabecera de la respuesta V1. - WriteObject(Main.VERSION, out) - WriteObject("query", out) - WriteInt(rs.ColumnCount, out) - ' Escribe los nombres de las columnas. - For i = 0 To cols - 1 - WriteObject(rs.GetColumnName(i), out) - Next - - ' Itera sobre las filas del resultado. - Do While rs.NextRow And limit > 0 - ' Escribe un byte '1' para indicar que viene una fila. - WriteByte(1, out) - ' Itera sobre las columnas de la fila. - For i = 0 To cols - 1 - Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1)) - ' Maneja los tipos de datos binarios de forma especial. - If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then - WriteObject(rs.GetBlob2(i), out) - Else - ' Escribe el valor de la columna. - WriteObject(jrs.RunMethod("getObject", Array(i + 1)), out) - End If - Next - limit = limit - 1 - Loop - ' Escribe un byte '0' para indicar el fin de las filas. - WriteByte(0, out) - out.Close - rs.Close - - Return "query: " & queryName + ' --- INICIO VALIDACIÓN DE PARÁMETROS (V1) --- + If theSql.Contains("?") Or (params <> Null And params.Size > 0) Then + Dim expectedParams As Int = theSql.Length - theSql.Replace("?", "").Length + Dim receivedParams As Int + If params = Null Then receivedParams = 0 Else receivedParams = params.Size + + If expectedParams <> receivedParams Then + Dim errorMessage As String = $"Número de parametros equivocado para "${queryName}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ + Log(errorMessage) + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If + End If + ' --- FIN VALIDACIÓN --- + + ' Ejecuta la consulta. + Dim rs As ResultSet = con.ExecQuery2(theSql, params) + + If limit <= 0 Then limit = 0x7fffffff 'max int + + Dim jrs As JavaObject = rs + Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) + Dim cols As Int = rs.ColumnCount + + Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime el stream de salida. + + ' Escribe la cabecera de la respuesta V1. + WriteObject(Main.VERSION, out) + WriteObject("query", out) + WriteInt(rs.ColumnCount, out) + + ' Escribe los nombres de las columnas. + For i = 0 To cols - 1 + WriteObject(rs.GetColumnName(i), out) + Next + + ' Itera sobre las filas del resultado. + Do While rs.NextRow And limit > 0 + WriteByte(1, out) ' Escribe un byte '1' para indicar que viene una fila. + ' Itera sobre las columnas de la fila. + For i = 0 To cols - 1 + Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1)) + ' Maneja los tipos de datos binarios de forma especial. + If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then + WriteObject(rs.GetBlob2(i), out) + Else + ' Escribe el valor de la columna. + WriteObject(jrs.RunMethod("getObject", Array(i + 1)), out) + End If + Next + limit = limit - 1 + Loop + + ' Escribe un byte '0' para indicar el fin de las filas. + WriteByte(0, out) + out.Close + rs.Close + + Return "query: " & queryName End Sub ' Escribe un único byte en el stream de salida. Private Sub WriteByte(value As Byte, out As OutputStream) - out.WriteBytes(Array As Byte(value), 0, 1) + out.WriteBytes(Array As Byte(value), 0, 1) End Sub ' Serializador principal para el protocolo V1. Escribe un objeto al stream. Private Sub WriteObject(o As Object, out As OutputStream) - Dim data() As Byte - ' Escribe un byte de tipo seguido de los datos. - If o = Null Then - out.WriteBytes(Array As Byte(T_NULL), 0, 1) - Else If o Is Short Then - out.WriteBytes(Array As Byte(T_SHORT), 0, 1) - data = bc.ShortsToBytes(Array As Short(o)) - Else If o Is Int Then - out.WriteBytes(Array As Byte(T_INT), 0, 1) - data = bc.IntsToBytes(Array As Int(o)) - Else If o Is Float Then - out.WriteBytes(Array As Byte(T_FLOAT), 0, 1) - data = bc.FloatsToBytes(Array As Float(o)) - Else If o Is Double Then - out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1) - data = bc.DoublesToBytes(Array As Double(o)) - Else If o Is Long Then - out.WriteBytes(Array As Byte(T_LONG), 0, 1) - data = bc.LongsToBytes(Array As Long(o)) - Else If o Is Boolean Then - out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1) - Dim b As Boolean = o - Dim data(1) As Byte - If b Then data(0) = 1 Else data(0) = 0 - Else If GetType(o) = "[B" Then ' Si el objeto es un array de bytes (BLOB) - data = o - out.WriteBytes(Array As Byte(T_BLOB), 0, 1) - ' Escribe la longitud de los datos antes de los datos mismos. - WriteInt(data.Length, out) - Else ' Trata todo lo demás como un String - out.WriteBytes(Array As Byte(T_STRING), 0, 1) - data = bc.StringToBytes(o, "UTF8") - ' Escribe la longitud del string antes del string. - WriteInt(data.Length, out) - End If - ' Escribe los bytes del dato. - If data.Length > 0 Then out.WriteBytes(data, 0, data.Length) + Dim data() As Byte + ' Escribe un byte de tipo seguido de los datos. + If o = Null Then + out.WriteBytes(Array As Byte(T_NULL), 0, 1) + Else If o Is Short Then + out.WriteBytes(Array As Byte(T_SHORT), 0, 1) + data = bc.ShortsToBytes(Array As Short(o)) + Else If o Is Int Then + out.WriteBytes(Array As Byte(T_INT), 0, 1) + data = bc.IntsToBytes(Array As Int(o)) + Else If o Is Float Then + out.WriteBytes(Array As Byte(T_FLOAT), 0, 1) + data = bc.FloatsToBytes(Array As Float(o)) + Else If o Is Double Then + out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1) + data = bc.DoublesToBytes(Array As Double(o)) + Else If o Is Long Then + out.WriteBytes(Array As Byte(T_LONG), 0, 1) + data = bc.LongsToBytes(Array As Long(o)) + Else If o Is Boolean Then + out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1) + Dim b As Boolean = o + Dim data(1) As Byte + If b Then data(0) = 1 Else data(0) = 0 + Else If GetType(o) = "[B" Then ' Si el objeto es un array de bytes (BLOB) + data = o + out.WriteBytes(Array As Byte(T_BLOB), 0, 1) + ' Escribe la longitud de los datos antes de los datos mismos. + WriteInt(data.Length, out) + Else ' Trata todo lo demás como un String + out.WriteBytes(Array As Byte(T_STRING), 0, 1) + data = bc.StringToBytes(o, "UTF8") + ' Escribe la longitud del string antes del string. + WriteInt(data.Length, out) + End If + ' Escribe los bytes del dato. + If data.Length > 0 Then out.WriteBytes(data, 0, data.Length) End Sub ' Deserializador principal para el protocolo V1. Lee un objeto del stream. Private Sub ReadObject(In As InputStream) As Object - ' Lee el primer byte para determinar el tipo de dato. - Dim data(1) As Byte - In.ReadBytes(data, 0, 1) - Select data(0) - Case T_NULL - Return Null - Case T_SHORT - Dim data(2) As Byte - Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0) - Case T_INT - Dim data(4) As Byte - Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) - Case T_LONG - Dim data(8) As Byte - Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0) - Case T_FLOAT - Dim data(4) As Byte - Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0) - Case T_DOUBLE - Dim data(8) As Byte - Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0) - Case T_BOOLEAN - Dim b As Byte = ReadByte(In) - Return b = 1 - Case T_BLOB - ' Lee la longitud, luego lee esa cantidad de bytes. - Dim len As Int = ReadInt(In) - Dim data(len) As Byte - Return ReadBytesFully(In, data, data.Length) - Case Else ' T_STRING - ' Lee la longitud, luego lee esa cantidad de bytes y los convierte a string. - Dim len As Int = ReadInt(In) - Dim data(len) As Byte - ReadBytesFully(In, data, data.Length) - Return BytesToString(data, 0, data.Length, "UTF8") - End Select + ' Lee el primer byte para determinar el tipo de dato. + Dim data(1) As Byte + In.ReadBytes(data, 0, 1) + Select data(0) + Case T_NULL + Return Null + Case T_SHORT + Dim data(2) As Byte + Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_INT + Dim data(4) As Byte + Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_LONG + Dim data(8) As Byte + Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_FLOAT + Dim data(4) As Byte + Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_DOUBLE + Dim data(8) As Byte + Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_BOOLEAN + Dim b As Byte = ReadByte(In) + Return b = 1 + Case T_BLOB + ' Lee la longitud, luego lee esa cantidad de bytes. + Dim len As Int = ReadInt(In) + Dim data(len) As Byte + Return ReadBytesFully(In, data, data.Length) + Case Else ' T_STRING + ' Lee la longitud, luego lee esa cantidad de bytes y los convierte a string. + Dim len As Int = ReadInt(In) + Dim data(len) As Byte + ReadBytesFully(In, data, data.Length) + Return BytesToString(data, 0, data.Length, "UTF8") + End Select End Sub ' Se asegura de leer exactamente la cantidad de bytes solicitada del stream. Private Sub ReadBytesFully(In As InputStream, Data() As Byte, Len As Int) As Byte() - Dim count = 0, Read As Int - ' Sigue leyendo en un bucle hasta llenar el buffer, por si los datos llegan en partes. - Do While count < Len And Read > -1 - Read = In.ReadBytes(Data, count, Len - count) - count = count + Read - Loop - Return Data + Dim count = 0, Read As Int + ' Sigue leyendo en un bucle hasta llenar el buffer, por si los datos llegan en partes. + Do While count < Len And Read > -1 + Read = In.ReadBytes(Data, count, Len - count) + count = count + Read + Loop + Return Data End Sub ' Escribe un entero (4 bytes) en el stream. Private Sub WriteInt(i As Int, out As OutputStream) - Dim data() As Byte - data = bc.IntsToBytes(Array As Int(i)) - out.WriteBytes(data, 0, data.Length) + Dim data() As Byte + data = bc.IntsToBytes(Array As Int(i)) + out.WriteBytes(data, 0, data.Length) End Sub ' Lee un entero (4 bytes) del stream. Private Sub ReadInt(In As InputStream) As Int - Dim data(4) As Byte - Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Dim data(4) As Byte + Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) End Sub ' Lee un solo byte del stream. Private Sub ReadByte(In As InputStream) As Byte - Dim data(1) As Byte - In.ReadBytes(data, 0, 1) - Return data(0) + Dim data(1) As Byte + In.ReadBytes(data, 0, 1) + Return data(0) End Sub ' Lee una lista de objetos del stream (protocolo V1). Private Sub ReadList(in As InputStream) As List - ' Primero lee la cantidad de elementos en la lista. - Dim len As Int = ReadInt(in) - Dim l1 As List - l1.Initialize - ' Luego lee cada objeto uno por uno y lo añade a la lista. - For i = 0 To len - 1 - l1.Add(ReadObject(in)) - Next - Return l1 + ' Primero lee la cantidad de elementos en la lista. + Dim len As Int = ReadInt(in) + Dim l1 As List + l1.Initialize + ' Luego lee cada objeto uno por uno y lo añade a la lista. + For i = 0 To len - 1 + l1.Add(ReadObject(in)) + Next + Return l1 End Sub -'#end If + +#end If ' Fin del bloque de compilación condicional para VERSION1 ' Envía una respuesta de error en formato de texto plano. ' Esto evita la página de error HTML por defecto que genera resp.SendError. ' resp: El objeto ServletResponse para enviar la respuesta. ' statusCode: El código de estado HTTP (ej. 400 para Bad Request, 500 para Internal Server Error). ' errorMessage: El mensaje de error que se enviará al cliente. +' En los clientes de B4X, una respuesta en HTML o JSON no es lo ideal, el IDE muestra todo el texto del error y texto plano es mucho mas facil de leer que HTML o JSON. Private Sub SendPlainTextError(resp As ServletResponse, statusCode As Int, errorMessage As String) Try ' Establece el código de estado HTTP (ej. 400, 500). resp.Status = statusCode - ' Define el tipo de contenido como texto plano, con codificación UTF-8 para soportar acentos. resp.ContentType = "text/plain; charset=utf-8" - ' Obtiene el OutputStream de la respuesta para escribir los datos directamente. Dim out As OutputStream = resp.OutputStream - ' Convierte el mensaje de error a un array de bytes usando UTF-8. Dim data() As Byte = errorMessage.GetBytes("UTF8") - ' Escribe los bytes en el stream de salida. out.WriteBytes(data, 0, data.Length) - ' Cierra el stream para asegurar que todos los datos se envíen correctamente. out.Close Catch @@ -651,4 +718,4 @@ Private Sub SendPlainTextError(resp As ServletResponse, statusCode As Int, error ' para que no se pierda la causa original del problema. Log("Error sending plain text error response: " & LastException) End Try -End Sub +End Sub \ No newline at end of file diff --git a/DBHandlerJSON.bas b/DBHandlerJSON.bas index f16f28a..d2f74f5 100644 --- a/DBHandlerJSON.bas +++ b/DBHandlerJSON.bas @@ -4,114 +4,134 @@ ModulesStructureVersion=1 Type=Class Version=10.3 @EndOfDesignText@ -' Handler class for JSON requests from Web Clients (JavaScript/axios) +' 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. + ' 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). ' 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) --- - resp.SetHeader("Access-Control-Allow-Origin", "*") - resp.SetHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - resp.SetHeader("Access-Control-Allow-Headers", "Content-Type") + ' 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 ' Las peticiones OPTIONS no incrementan contadores ni usan BD, así que salimos directamente. + Return ' Salimos directamente para estas peticiones. End If - Dim start As Long = DateTime.Now + 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 la limpieza. + ' 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 de la petición, calculada antes del log. + 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" - Dim requestsBeforeDecrement As Int = 0 ' Se inicializa en 0. + 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. 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) - jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") - Is0.Close + 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 SendErrorResponse(resp, 400, "Falta el parámetro 'j' en el URL o el cuerpo JSON en la petición.") duration = DateTime.Now - start + ' Llama a CleanupAndLog para registrar que hubo un error, pero con contadores a 0 o inicializados. CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) - Return ' Salida temprana. + Return ' Salida temprana si no hay JSON válido. End If Dim parser As JSONParser - parser.Initialize(jsonString) - Dim RootMap As Map = parser.NextObject - Dim execType As String = RootMap.GetDefault("exec", "") - - queryNameForLog = RootMap.GetDefault("query", "") '[___new 3.txt, 203] - If queryNameForLog = "" Then queryNameForLog = RootMap.GetDefault("exec", "unknown_json_command") '[___new 3.txt, 203] + 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 paramsList As List = RootMap.Get("params") + 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 + paramsList.Initialize ' Si no hay parámetros, inicializa una lista vacía. End If - ' <<<< ¡CORRECCIÓN CLAVE AQUÍ: RESOLVEMOS finalDbKey del JSON ANTES! >>>> - If RootMap.Get("dbx") <> Null Then finalDbKey = RootMap.Get("dbx") '[___new 3.txt, 204] + ' <<<< ¡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) --- - ' 1. Aseguramos que el valor inicial sea un Int y lo recuperamos como Int. + ' 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 = 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}"$) + ' 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 --- - - Connector = Main.Connectors.Get(finalDbKey) ' Inicializamos el Connector con la finalDbKey resuelta. + ' 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 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. + Return ' Salida temprana si la DB no es válida. End If - 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. >>>> + 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 '[___new 3.txt, 204] + Dim poolStats As Map = Connector.GetPoolStats If poolStats.ContainsKey("BusyConnections") Then - poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Aseguramos que sea Int. + ' <<<< ¡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! >>>> + ' <<<< ¡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) @@ -121,8 +141,10 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) Return ' Salida temprana. End If + ' --- Lógica para ejecutar diferentes tipos de comandos basados en el parámetro 'execType' --- If execType.ToLowerCase = "executequery" Then Dim rs As ResultSet + ' Validación de parámetros para ExecuteQuery. If sqlCommand.Contains("?") Or paramsList.Size > 0 Then Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length Dim receivedParams As Int = paramsList.Size @@ -133,31 +155,32 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) Return ' Salida temprana. End If - rs = con.ExecQuery2(sqlCommand, paramsList) + rs = con.ExecQuery2(sqlCommand, paramsList) ' Ejecuta la consulta con parámetros. Else - rs = con.ExecQuery(sqlCommand) + rs = con.ExecQuery(sqlCommand) ' Ejecuta la consulta sin parámetros. End If 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) + 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 + Do While rs.NextRow ' Itera sobre cada fila del resultado. Dim RowMap As Map - RowMap.Initialize - For i = 1 To cols - Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i)) - Dim value As Object = jrs.RunMethod("getObject", Array(i)) - RowMap.Put(ColumnName, value) + 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) + ResultList.Add(RowMap) ' Añade el mapa de la fila a la lista de resultados. Loop - rs.Close - SendSuccessResponse(resp, CreateMap("result": ResultList)) + rs.Close ' Cierra el ResultSet. + SendSuccessResponse(resp, CreateMap("result": ResultList)) ' Envía la respuesta JSON de éxito. Else If execType.ToLowerCase = "executecommand" Then + ' Validación de parámetros para ExecuteCommand. If sqlCommand.Contains("?") Then Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length Dim receivedParams As Int = paramsList.Size @@ -168,58 +191,74 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) Return ' Salida temprana. End If End If - con.ExecNonQuery2(sqlCommand, paramsList) - SendSuccessResponse(resp, CreateMap("message": "Command executed successfully")) + con.ExecNonQuery2(sqlCommand, paramsList) ' Ejecuta un comando (INSERT, UPDATE, DELETE). + SendSuccessResponse(resp, CreateMap("message": "Command executed successfully")) ' Envía confirmación de éxito. Else + ' Si el tipo de ejecución no es reconocido. 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: Maneja errores generales de ejecución o de SQL/JSON --- - Log(LastException) - SendErrorResponse(resp, 500, LastException.Message) + ' Si ocurre una excepción inesperada durante el procesamiento de la petición. + Log(LastException) ' Registra la excepción completa en el 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) --- - duration = DateTime.Now - start + ' 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 y la limpieza --- +' --- 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) -' 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] + ' 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 AQUÍ: Aseguramos que currentCount sea Int! >>>> + ' <<<< ¡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: dbKey=${dbKey}, currentCount (as Int)=${currentCount}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) + ' 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: dbKey=${dbKey}, New count (as Int)=${GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey,0).As(Int)}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) + ' 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. + ' 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. @@ -227,17 +266,25 @@ Private Sub SendSuccessResponse(resp As ServletResponse, dataMap As Map) 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 + 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 +End Sub \ No newline at end of file diff --git a/Manager.bas b/Manager.bas index e660b09..3dc59db 100644 --- a/Manager.bas +++ b/Manager.bas @@ -4,25 +4,39 @@ ModulesStructureVersion=1 Type=Class Version=8.8 @EndOfDesignText@ -'Handler class +' Módulo de clase: Manager +' Este handler proporciona un panel de administración web para el servidor jRDC2-Multi. +' Permite monitorear el estado del servidor, recargar configuraciones de bases de datos, +' ver estadísticas de rendimiento, reiniciar servicios externos, y gestionar la autenticación de usuarios. + Sub Class_Globals + ' Objeto para generar respuestas JSON. Se utiliza para mostrar mapas de datos de forma legible. Dim j As JSONGenerator -' Dim rdcc As RDCConnector + ' La clase BCrypt no se usa directamente en este módulo, pero se mantiene si hubiera planes futuros. + ' Private bc As BCrypt 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 +' Método principal que maneja las peticiones HTTP para el panel de administración. +' req: El objeto ServletRequest que contiene la información de la petición entrante. +' resp: El objeto ServletResponse para construir y enviar la respuesta al cliente. Sub Handle(req As ServletRequest, resp As ServletResponse) - ' 1. --- Bloque de Seguridad --- + ' --- 1. Bloque de Seguridad: Autenticación de Usuario --- + ' Verifica si el usuario actual ha iniciado sesión y está autorizado. + ' Si no está autorizado, se le redirige a la página de login. If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then resp.SendRedirect("/login") - Return + Return ' Termina la ejecución si no está autorizado. End If + ' Obtiene el comando solicitado de los parámetros de la URL (ej. "?command=reload"). Dim Command As String = req.GetParameter("command") - If Command = "" Then Command = "ping" + If Command = "" Then Command = "ping" ' Si no se especifica un comando, por defecto es "ping". + Log($"Command: ${Command}"$) ' --- MANEJO ESPECIAL PARA SNAPSHOT --- @@ -46,9 +60,9 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) End If ' --- FIN DE MANEJO ESPECIAL --- - ' Para todos los demás comandos, construimos la página HTML - resp.ContentType = "text/html" - Dim sb As StringBuilder + ' Para todos los demás comandos, construimos la página HTML de respuesta. + resp.ContentType = "text/html" ' Establece el tipo de contenido como HTML. + Dim sb As StringBuilder ' Usamos StringBuilder para construir eficientemente el HTML. sb.Initialize ' --- Estilos y JavaScript (igual que antes) --- @@ -64,12 +78,23 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) sb.Append("") sb.Append("
") - ' --- Cabecera, Botón y Formulario Oculto (igual que antes) --- + ' --- Cabecera de la Página y Mensaje de Bienvenida --- sb.Append("Bienvenido, ${req.GetSession.GetAttribute("username")}
"$) + + ' --- Menú de Navegación del Manager --- + ' Este menú incluye las opciones para interactuar con el servidor. + sb.Append("") + sb.Append("