mirror of
https://github.com/KeymonSoft/jRDC-Multi.git
synced 2026-04-17 21:06:24 +00:00
- VERSION 5.09.15
1. **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. 2. **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.
This commit is contained in:
222
Cambios.bas
222
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
|
||||
@@ -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)
|
||||
|
||||
759
DBHandlerB4X.bas
759
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
|
||||
@@ -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
|
||||
65
Manager.bas
65
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("<script>function toggleForm() {var form = document.getElementById('changePassForm'); if (form.style.display === 'none') {form.style.display = 'block';} else {form.style.display = 'none';}}</script>")
|
||||
sb.Append("</head><body>")
|
||||
|
||||
' --- Cabecera, Botón y Formulario Oculto (igual que antes) ---
|
||||
' --- Cabecera de la Página y Mensaje de Bienvenida ---
|
||||
sb.Append("<h1>Panel de Administración jRDC</h1>")
|
||||
sb.Append($"Bienvenido, <b>${req.GetSession.GetAttribute("username")}</b><br>"$)
|
||||
' sb.Append("<p class='nav'><a href='/manager?command=test'>Test</a> | <a href='/manager?command=ping'>Ping</a> | <a href='/manager?command=reload'>Reload</a> | <a href='/manager?command=rpm2'>Reiniciar (pm2)</a> | <a href='/manager?command=reviveBow'>Revive Bow</a></p><hr>")
|
||||
sb.Append("<p class='nav'><a href='/manager?command=test'>Test</a> | <a href='/manager?command=ping'>Ping</a> | <a href='/manager?command=reload'>Reload</a> | <a href='/manager?command=slowqueries'>Queries Lentos</a> | <a href='/manager?command=totalcon'>Estadísticas Pool</a> | <a href='/manager?command=rpm2'>Reiniciar (pm2)</a> | <a href='/manager?command=reviveBow'>Revive Bow</a></p><hr>")
|
||||
' sb.Append("<button onclick='toggleForm()'>Cambiar Contraseña</button>")
|
||||
sb.Append($"<p>Bienvenido, <strong>${req.GetSession.GetAttribute("username")}</strong></p>"$)
|
||||
|
||||
' --- Menú de Navegación del Manager ---
|
||||
' Este menú incluye las opciones para interactuar con el servidor.
|
||||
sb.Append("<div class='menu'>")
|
||||
sb.Append("<a href='/manager?command=test'>Test</a> | ")
|
||||
sb.Append("<a href='/manager?command=ping'>Ping</a> | ")
|
||||
sb.Append("<a href='/manager?command=reload'>Reload</a> | ")
|
||||
sb.Append("<a href='/manager?command=slowqueries'>Queries Lentas</a> | ") ' Nuevo enlace para queries lentas.
|
||||
sb.Append("<a href='/manager?command=totalcon'>Estadísticas Pool</a> | ") ' Nuevo enlace para estadísticas del pool.
|
||||
sb.Append("<a href='/manager?command=rpm2'>Reiniciar (pm2)</a> | ")
|
||||
sb.Append("<a href='/manager?command=reviveBow'>Revive Bow</a>")
|
||||
sb.Append("</div>")
|
||||
sb.Append("<hr>")
|
||||
|
||||
sb.Append("<div id='changePassForm' style='display:none;'>")
|
||||
sb.Append("<h2>Cambiar Contraseña</h2><form action='/changepass' method='post'>")
|
||||
sb.Append("Contraseña Actual: <input type='password' name='current_password' required><br>")
|
||||
@@ -93,12 +118,12 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
|
||||
' 1. Crear un nuevo mapa temporal para almacenar los conectores recién inicializados.
|
||||
Dim newConnectors As Map
|
||||
newConnectors.Initialize
|
||||
|
||||
|
||||
' Guardamos una referencia al mapa de conectores actualmente activos.
|
||||
Dim oldConnectors As Map
|
||||
|
||||
|
||||
Dim reloadSuccessful As Boolean = True
|
||||
|
||||
|
||||
' *** INICIO DEL BLOQUE CRÍTICO 1: Obtener oldConnectors con ReentrantLock ***
|
||||
Dim lock1Acquired As Boolean = False ' Bandera para saber si el bloqueo fue adquirido.
|
||||
Try
|
||||
@@ -119,7 +144,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
|
||||
sb.Append($"¡ERROR: La recarga de configuración falló en la fase de bloqueo inicial! Los conectores antiguos siguen activos."$).Append("<br>" & CRLF)
|
||||
Return ' Salir del Handle ya que ocurrió un error crítico irrecuperable.
|
||||
End If
|
||||
|
||||
|
||||
' 2. Iterar sobre las bases de datos configuradas y crear *nuevas* instancias de RDCConnector.
|
||||
For Each dbKey As String In Main.listaDeCP
|
||||
Try
|
||||
@@ -129,14 +154,14 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
|
||||
|
||||
Dim newPoolStats As Map = newRDC.GetPoolStats
|
||||
sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append("<br>" & CRLF)
|
||||
|
||||
|
||||
Catch
|
||||
sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append("<br>" & CRLF)
|
||||
reloadSuccessful = False
|
||||
Exit ' Si uno falla, abortamos la recarga completa para evitar un estado inconsistente.
|
||||
End Try
|
||||
Next
|
||||
|
||||
|
||||
sb.Append(sbTemp.ToString) ' Añadimos los logs acumulados de la inicialización al StringBuilder principal.
|
||||
|
||||
If reloadSuccessful Then
|
||||
|
||||
211
RDCConnector.bas
211
RDCConnector.bas
@@ -5,45 +5,61 @@ Type=Class
|
||||
Version=4.19
|
||||
@EndOfDesignText@
|
||||
' Módulo de clase: RDCConnector
|
||||
' Esta clase gestiona el pool de conexiones a una base de datos específica.
|
||||
' Cada instancia de RDCConnector maneja la conexión y los comandos para una base de datos.
|
||||
' Esta clase gestiona el pool de conexiones a una base de datos específica utilizando la librería C3P0.
|
||||
' Cada instancia de RDCConnector es responsable de una base de datos definida en un archivo 'config.DBx.properties'.
|
||||
' Se encarga de inicializar el pool, obtener conexiones, cargar comandos SQL y proporcionar estadísticas del pool.
|
||||
|
||||
Sub Class_Globals
|
||||
Private pool As ConnectionPool ' Objeto principal para gestionar el pool de conexiones de la base de datos (usa C3P0 internamente).
|
||||
Private DebugQueries As Boolean ' Bandera para activar/desactivar el modo de depuración de queries.
|
||||
Dim commands As Map ' Almacena los comandos SQL específicos de esta base de datos, cargados de su archivo de configuración.
|
||||
Public serverPort As Int ' El puerto que el servidor HTTP usará, obtenido del archivo de configuración principal (config.properties).
|
||||
Public usePool As Boolean = True ' Indica si se debe usar el pool de conexiones (siempre True en este diseño).
|
||||
Dim config As Map ' Almacena la configuración completa (JdbcUrl, User, Password, etc.) cargada de su respectivo archivo .properties.
|
||||
' --- Variables globales de la clase ---
|
||||
|
||||
' Objeto principal para gestionar el pool de conexiones de la base de datos (usa C3P0 internamente).
|
||||
Private pool As ConnectionPool
|
||||
|
||||
' Bandera para activar/desactivar el modo de depuración de queries.
|
||||
' Cuando está en True, los comandos SQL se recargan en cada petición (útil en desarrollo).
|
||||
Private DebugQueries As Boolean
|
||||
|
||||
' Almacena los comandos SQL específicos de esta base de datos, cargados de su archivo de configuración.
|
||||
Public commands As Map
|
||||
|
||||
' El puerto que el servidor HTTP usará. Este valor se lee del 'config.properties' de la base de datos principal (DB1).
|
||||
Public serverPort As Int
|
||||
|
||||
' Indica si se debe usar el pool de conexiones. Siempre True en este diseño, ya que C3P0 es esencial.
|
||||
Public usePool As Boolean = True
|
||||
|
||||
' Almacena la configuración completa (DriverClass, JdbcUrl, User, Password, InitialPoolSize, etc.)
|
||||
' cargada de su respectivo archivo .properties.
|
||||
Public config As Map
|
||||
End Sub
|
||||
|
||||
' Subrutina de inicialización para el conector de una base de datos específica.
|
||||
' Se llama una vez por cada base de datos (DB1, DB2, DB3, DB4) al iniciar el servidor.
|
||||
' Se llama una vez por cada base de datos (DB1, DB2, DB3, DB4) al iniciar el servidor en Main.AppStart.
|
||||
' DB: El identificador único de la base de datos (ej. "DB1", "DB2").
|
||||
Public Sub Initialize(DB As String)
|
||||
' Si el identificador es "DB1", se usa una cadena vacía para que File.ReadMap cargue "config.properties" (el archivo por defecto).
|
||||
If DB.EqualsIgnoreCase("DB1") Then DB = ""
|
||||
|
||||
|
||||
' PASO 1: Cargar la configuración desde el archivo .properties correspondiente.
|
||||
' Es CRUCIAL que se asigne a la variable de CLASE 'config' (sin 'Dim' local)
|
||||
' para que la configuración cargada del archivo sea persistente para esta instancia del conector.
|
||||
config = LoadConfigMap(DB)
|
||||
|
||||
|
||||
' Bloque Try-Catch para la inicialización y configuración del pool.
|
||||
' Esto capturará cualquier error crítico que impida la conexión inicial a la base de datos.
|
||||
' Esto es esencial para capturar cualquier error crítico que impida la conexión inicial a la base de datos.
|
||||
Try
|
||||
' PASO 2: Inicializar el objeto B4X ConnectionPool.
|
||||
' Esto crea la instancia subyacente de com.mchange.v2.c3p0.ComboPooledDataSource (la librería C3P0).
|
||||
' En este punto, C3P0 solo se inicializa como objeto. Aún no intenta hacer conexiones activas.
|
||||
' Se le pasan los parámetros básicos para que C3P0 pueda construirse.
|
||||
pool.Initialize(config.Get("DriverClass"), config.Get("JdbcUrl"), config.Get("User"), config.Get("Password"))
|
||||
|
||||
Dim jo As JavaObject = pool ' Obtener la referencia JavaObject para acceder a métodos de configuración avanzados de C3P0.
|
||||
|
||||
|
||||
' Obtener la referencia JavaObject para acceder a métodos de configuración avanzados de C3P0.
|
||||
Dim jo As JavaObject = pool
|
||||
|
||||
' PASO 3: Aplicar *todas* las propiedades de configuración de C3P0 INMEDIATAMENTE.
|
||||
' Esto debe ocurrir *después* de 'pool.Initialize' pero *antes* de que C3P0 intente realmente adquirir conexiones.
|
||||
' Esto asegura que las configuraciones sean efectivas desde el primer intento de conexión.
|
||||
|
||||
|
||||
' Lectura de los valores desde el archivo de configuración, con valores por defecto si no se encuentran.
|
||||
Dim initialPoolSize As Int = config.GetDefault("InitialPoolSize", 3)
|
||||
Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2)
|
||||
@@ -51,24 +67,24 @@ Public Sub Initialize(DB As String)
|
||||
Dim acquireIncrement As Int = config.GetDefault("AcquireIncrement", 5)
|
||||
|
||||
' Configuración de los parámetros del pool de conexiones C3P0:
|
||||
jo.RunMethod("setInitialPoolSize", Array(initialPoolSize)) ' Define el número de conexiones que se intentarán crear al iniciar el pool.
|
||||
jo.RunMethod("setMinPoolSize", Array(minPoolSize)) ' Fija el número mínimo de conexiones que el pool mantendrá abiertas.
|
||||
jo.RunMethod("setMaxPoolSize", Array(maxPoolSize)) ' Define el número máximo de conexiones simultáneas.
|
||||
jo.RunMethod("setAcquireIncrement", Array(acquireIncrement)) ' Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
|
||||
jo.RunMethod("setMaxIdleTime", Array As Object(config.GetDefault("MaxIdleTime", 300))) ' Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
|
||||
jo.RunMethod("setInitialPoolSize", Array(initialPoolSize)) ' Define el número de conexiones que se intentarán crear al iniciar el pool.
|
||||
jo.RunMethod("setMinPoolSize", Array(minPoolSize)) ' Fija el número mínimo de conexiones que el pool mantendrá abiertas.
|
||||
jo.RunMethod("setMaxPoolSize", Array(maxPoolSize)) ' Define el número máximo de conexiones simultáneas.
|
||||
jo.RunMethod("setAcquireIncrement", Array(acquireIncrement)) ' Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
|
||||
jo.RunMethod("setMaxIdleTime", Array As Object(config.GetDefault("MaxIdleTime", 300))) ' Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
|
||||
jo.RunMethod("setMaxConnectionAge", Array As Object(config.GetDefault("MaxConnectionAge", 900))) ' Tiempo máximo de vida de una conexión (segundos).
|
||||
jo.RunMethod("setCheckoutTimeout", Array As Object(config.GetDefault("CheckoutTimeout", 60000))) ' Tiempo máximo de espera por una conexión del pool (milisegundos).
|
||||
|
||||
|
||||
' LÍNEAS CRÍTICAS PARA FORZAR UN COMPORTAMIENTO NO SILENCIOSO DE C3P0:
|
||||
' Por defecto, C3P0 puede reintentar muchas veces y no lanzar una excepción si las conexiones iniciales fallan.
|
||||
' Estas líneas fuerzan a C3P0 a ser estricto y reportar errores de inmediato.
|
||||
jo.RunMethod("setAcquireRetryAttempts", Array As Object(1)) ' Limita los intentos iniciales de adquisición a 1.
|
||||
jo.RunMethod("setAcquireRetryAttempts", Array As Object(1)) ' Limita los intentos iniciales de adquisición a 1.
|
||||
jo.RunMethod("setBreakAfterAcquireFailure", Array As Object(True)) ' ¡Forza a C3P0 a lanzar una excepción si falla al adquirir conexiones!
|
||||
|
||||
|
||||
' PASO 4: Forzar la creación de conexiones iniciales y verificar el estado.
|
||||
' Este paso es VITAL. Obliga a C3P0 a intentar establecer las conexiones iniciales (InitialPoolSize)
|
||||
' *con la configuración ya establecida*. Si hay un problema de conectividad real, la excepción
|
||||
' se capturará aquí y se reportará.
|
||||
' se capturará aquí y se reportará, evitando "fallos silenciosos".
|
||||
Dim tempCon As SQL = pool.GetConnection ' Adquiere una conexión para forzar al pool a inicializarse.
|
||||
If tempCon.IsInitialized Then
|
||||
tempCon.Close ' Devolvemos la conexión inmediatamente al pool para que esté disponible.
|
||||
@@ -118,16 +134,16 @@ Public Sub Initialize(DB As String)
|
||||
' userOverrides -> {},
|
||||
' usesTraditionalReflectiveProxies -> False
|
||||
' ]
|
||||
'
|
||||
'
|
||||
Catch
|
||||
' Si ocurre un error durante la inicialización del pool o al forzar la conexión,
|
||||
' este Log es CRÍTICO para el diagnóstico, especialmente en un entorno de producción.
|
||||
Log($"RDCConnector.Initialize para ${DB}: ERROR CRÍTICO al inicializar/forzar conexión: ${LastException.Message}"$)
|
||||
End Try
|
||||
|
||||
|
||||
' Configuración de depuración de queries. Se activa automáticamente si el proyecto se ejecuta en modo DEBUG.
|
||||
#If DEBUG
|
||||
' DebugQueries = True
|
||||
' DebugQueries = True ' Descomentar para activar la recarga de comandos en cada petición en desarrollo.
|
||||
#Else
|
||||
DebugQueries = False
|
||||
#End If
|
||||
@@ -135,11 +151,12 @@ Public Sub Initialize(DB As String)
|
||||
' Se obtiene el puerto del servidor HTTP desde la configuración de esta base de datos.
|
||||
' Nota: En el diseño actual, el puerto principal lo define DB1 (config.properties).
|
||||
serverPort = config.Get("ServerPort")
|
||||
|
||||
|
||||
' Asegura que el identificador DB no sea una cadena vacía para la carga de comandos.
|
||||
' Esto es relevante si DB era "DB1" y se convirtió a "" al inicio de esta subrutina.
|
||||
If DB = "" Then DB = "DB1"
|
||||
|
||||
' Carga los comandos SQL predefinidos de esta base de datos en el mapa global 'commandsMap'.
|
||||
' Carga los comandos SQL predefinidos de esta base de datos en el mapa global 'commandsMap' de Main.
|
||||
LoadSQLCommands(config, DB)
|
||||
End Sub
|
||||
|
||||
@@ -149,7 +166,7 @@ End Sub
|
||||
Private Sub LoadConfigMap(DB As String) As Map
|
||||
Private DBX As String = ""
|
||||
If DB <> "" Then DBX = "." & DB ' Construye el sufijo del nombre de archivo (ej. ".DB2").
|
||||
Log($"Leemos el config${DBX}.properties"$) ' Mantenemos este log para confirmación de carga.
|
||||
Log($"RDCConnector.LoadConfigMap: Leemos el config${DBX}.properties"$) ' Mantenemos este log para confirmación de carga.
|
||||
Return File.ReadMap("./", "config" & DBX & ".properties")
|
||||
End Sub
|
||||
|
||||
@@ -158,9 +175,10 @@ End Sub
|
||||
' Key: El nombre del comando SQL (ej. "select_user").
|
||||
' Retorna la sentencia SQL como String.
|
||||
Public Sub GetCommand(DB As String, Key As String) As String
|
||||
commands = Main.commandsMap.get(DB).As(Map) ' Obtiene los comandos de la DB específica del mapa global.
|
||||
' Obtiene los comandos de la DB específica del mapa global Main.commandsMap.
|
||||
commands = Main.commandsMap.Get(DB).As(Map)
|
||||
If commands.ContainsKey("sql." & Key) = False Then
|
||||
Log("*** Command not found: " & Key) ' Este log es importante mantenerlo si un comando no se encuentra.
|
||||
Log($"RDCConnector.GetCommand: *** Comando no encontrado: '${Key}' para DB: '${DB}' ***"$) ' Log importante si un comando no se encuentra.
|
||||
End If
|
||||
Return commands.Get("sql." & Key) ' Retorna la sentencia SQL.
|
||||
End Sub
|
||||
@@ -168,128 +186,131 @@ End Sub
|
||||
' Obtiene una conexión SQL funcional del pool de conexiones para la base de datos especificada.
|
||||
' DB: El identificador de la base de datos.
|
||||
' Retorna un objeto SQL (la conexión a la base de datos).
|
||||
'Public Sub GetConnection(DB As String) As SQL
|
||||
' If DB.EqualsIgnoreCase("DB1") Then DB = ""
|
||||
' ' En modo de depuración, recarga los comandos SQL en cada petición.
|
||||
' ' Esto permite modificar queries en config.properties sin reiniciar el servidor.
|
||||
' If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB)
|
||||
' Return pool.GetConnection ' Retorna una conexión del pool.
|
||||
'End Sub
|
||||
|
||||
Public Sub GetConnection(DB As String) As SQL
|
||||
If DB.EqualsIgnoreCase("DB1") Then DB = ""
|
||||
If DB.EqualsIgnoreCase("DB1") Then DB = ""
|
||||
|
||||
' If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB) ' Esta línea es condicional a DebugQueries
|
||||
' En modo de depuración, recarga los comandos SQL en cada petición.
|
||||
' Esto permite modificar queries en config.properties sin reiniciar el servidor durante el desarrollo.
|
||||
If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB)
|
||||
|
||||
' <<<< ¡ESTOS SON LOS LOGS QUE NECESITAMOS VER! >>>>
|
||||
' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Solicitando conexión del pool..."$)
|
||||
Dim conn As SQL = pool.GetConnection
|
||||
' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Conexión obtenida. IsInitialized: ${conn.IsInitialized}"$)
|
||||
' <<<< Bloque de Logs de Depuración de Adquisición de Conexión (descomentar si es necesario) >>>>
|
||||
' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Solicitando conexión del pool..."$)
|
||||
Dim conn As SQL = pool.GetConnection
|
||||
' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Conexión obtenida. IsInitialized: ${conn.IsInitialized}"$)
|
||||
|
||||
If pool.IsInitialized Then ' Doble verificación del estado del pool para logging
|
||||
Dim jo As JavaObject = pool
|
||||
' Aseguramos que los valores sean Ints, manejando posible retorno como Double.
|
||||
Dim busyCount As Int = jo.RunMethod("getNumBusyConnectionsAllUsers", Null).As(Object).As(Int)
|
||||
Dim totalCount As Int = jo.RunMethod("getNumConnectionsAllUsers", Null).As(Object).As(Int)
|
||||
' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Estadísticas del Pool (después de obtener): Busy=${busyCount}, Total=${totalCount}"$)
|
||||
End If
|
||||
' <<<< ¡FIN DE LOS LOGS A BUSCAR! >>>>
|
||||
|
||||
Return conn
|
||||
If pool.IsInitialized Then ' Doble verificación del estado del pool para logging más seguro
|
||||
' Dim jo As JavaObject = pool
|
||||
' Aseguramos que los valores de C3P0 sean Ints, manejando posibles retornos como Double.
|
||||
' Dim busyCount As Int = jo.RunMethod("getNumBusyConnectionsAllUsers", Null).As(Object).As(Int)
|
||||
' Dim totalCount As Int = jo.RunMethod("getNumConnectionsAllUsers", Null).As(Object).As(Int)
|
||||
' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Estadísticas del Pool (después de obtener): Busy=${busyCount}, Total=${totalCount}"$)
|
||||
End If
|
||||
' <<<< Fin del bloque de Logs de Depuración >>>>
|
||||
|
||||
Return conn ' Retorna una conexión del pool.
|
||||
End Sub
|
||||
|
||||
' Carga todos los comandos SQL del mapa de configuración en el mapa global 'commandsMap'.
|
||||
' config2: El mapa de configuración de la DB actual.
|
||||
' Carga todos los comandos SQL del mapa de configuración en el mapa global 'commandsMap' de Main.
|
||||
' config2: El mapa de configuración de la DB actual (JdbcUrl, User, Password, etc.).
|
||||
' DB: El identificador de la base de datos.
|
||||
Private Sub LoadSQLCommands(config2 As Map, DB As String)
|
||||
Dim newCommands As Map
|
||||
newCommands.Initialize
|
||||
|
||||
For Each k As String In config2.Keys
|
||||
If k.StartsWith("sql.") Then ' Busca claves que comiencen con "sql." (ej. "sql.select_user").
|
||||
newCommands.Put(k, config2.Get(k)) ' Añade el comando al mapa.
|
||||
End If
|
||||
Next
|
||||
|
||||
commands = newCommands ' Actualiza el mapa de comandos de esta instancia de RDCConnector.
|
||||
Main.commandsMap.Put(DB, commands) ' Almacena el mapa de comandos en el mapa global 'commandsMap' de Main.
|
||||
End Sub
|
||||
|
||||
' Nuevo: Obtiene estadísticas detalladas del pool de conexiones.
|
||||
' Es utilizado por el Manager para mostrar el estado del pool.
|
||||
Public Sub GetPoolStats As Map
|
||||
Dim stats As Map
|
||||
stats.Initialize
|
||||
' Log("--- RDCConnector.GetPoolStats llamado ---") ' Log de inicio
|
||||
|
||||
|
||||
' Log("--- RDCConnector.GetPoolStats llamado ---") ' Log de inicio (descomentar si es necesario)
|
||||
|
||||
If pool.IsInitialized Then
|
||||
' Log("RDCConnector.GetPoolStats: Pool está inicializado. Intentando obtener métricas.")
|
||||
Dim jo As JavaObject = pool ' Convertimos el objeto pool a JavaObject para acceder a sus métodos.
|
||||
' Log("RDCConnector.GetPoolStats: Pool está inicializado. Intentando obtener métricas.") ' Log (descomentar si es necesario)
|
||||
Dim jo As JavaObject = pool ' Convertimos el objeto pool a JavaObject para acceder a sus métodos internos de C3P0.
|
||||
Try
|
||||
' --- Métricas en tiempo real del pool ---
|
||||
' Se obtienen los valores y se aseguran como objetos para su posterior manejo en el mapa.
|
||||
Dim totalConn As Object = jo.RunMethod("getNumConnectionsAllUsers", Null)
|
||||
stats.Put("TotalConnections", totalConn)
|
||||
' Log($"RDCConnector.GetPoolStats: TotalConnections = ${totalConn}"$)
|
||||
|
||||
' Log($"RDCConnector.GetPoolStats: TotalConnections = ${totalConn}"$) ' Log (descomentar si es necesario)
|
||||
|
||||
Dim busyConn As Object = jo.RunMethod("getNumBusyConnectionsAllUsers", Null)
|
||||
stats.Put("BusyConnections", busyConn)
|
||||
' Log($"RDCConnector.GetPoolStats: BusyConnections = ${busyConn}"$)
|
||||
' Log($"RDCConnector.GetPoolStats: BusyConnections = ${busyConn}"$) ' Log (descomentar si es necesario)
|
||||
|
||||
Dim idleConn As Object = jo.RunMethod("getNumIdleConnectionsAllUsers", Null)
|
||||
stats.Put("IdleConnections", idleConn)
|
||||
' Log($"RDCConnector.GetPoolStats: IdleConnections = ${idleConn}"$)
|
||||
|
||||
' Log($"RDCConnector.GetPoolStats: IdleConnections = ${idleConn}"$) ' Log (descomentar si es necesario)
|
||||
|
||||
' --- Valores de configuración del pool (para referencia) ---
|
||||
' Se obtienen y almacenan los parámetros de configuración del pool.
|
||||
Dim initialSize As Object = jo.RunMethod("getInitialPoolSize", Null)
|
||||
stats.Put("InitialPoolSize", initialSize)
|
||||
' Log($"RDCConnector.GetPoolStats: InitialPoolSize = ${initialSize}"$)
|
||||
|
||||
' Log($"RDCConnector.GetPoolStats: InitialPoolSize = ${initialSize}"$) ' Log (descomentar si es necesario)
|
||||
|
||||
Dim minSize As Object = jo.RunMethod("getMinPoolSize", Null)
|
||||
stats.Put("MinPoolSize", minSize)
|
||||
' Log($"RDCConnector.GetPoolStats: MinPoolSize = ${minSize}"$)
|
||||
|
||||
' Log($"RDCConnector.GetPoolStats: MinPoolSize = ${minSize}"$) ' Log (descomentar si es necesario)
|
||||
|
||||
Dim maxSize As Object = jo.RunMethod("getMaxPoolSize", Null)
|
||||
stats.Put("MaxPoolSize", maxSize)
|
||||
' Log($"RDCConnector.GetPoolStats: MaxPoolSize = ${maxSize}"$)
|
||||
|
||||
' Log($"RDCConnector.GetPoolStats: MaxPoolSize = ${maxSize}"$) ' Log (descomentar si es necesario)
|
||||
|
||||
Dim acquireInc As Object = jo.RunMethod("getAcquireIncrement", Null)
|
||||
stats.Put("AcquireIncrement", acquireInc)
|
||||
' Log($"RDCConnector.GetPoolStats: AcquireIncrement = ${acquireInc}"$)
|
||||
|
||||
' Log($"RDCConnector.GetPoolStats: AcquireIncrement = ${acquireInc}"$) ' Log (descomentar si es necesario)
|
||||
|
||||
Dim maxIdle As Object = jo.RunMethod("getMaxIdleTime", Null)
|
||||
stats.Put("MaxIdleTime", maxIdle)
|
||||
' Log($"RDCConnector.GetPoolStats: MaxIdleTime = ${maxIdle}"$)
|
||||
|
||||
' Log($"RDCConnector.GetPoolStats: MaxIdleTime = ${maxIdle}"$) ' Log (descomentar si es necesario)
|
||||
|
||||
Dim maxAge As Object = jo.RunMethod("getMaxConnectionAge", Null)
|
||||
stats.Put("MaxConnectionAge", maxAge)
|
||||
' Log($"RDCConnector.GetPoolStats: MaxConnectionAge = ${maxAge}"$)
|
||||
|
||||
' Log($"RDCConnector.GetPoolStats: MaxConnectionAge = ${maxAge}"$) ' Log (descomentar si es necesario)
|
||||
|
||||
Dim checkoutTime As Object = jo.RunMethod("getCheckoutTimeout", Null)
|
||||
stats.Put("CheckoutTimeout", checkoutTime)
|
||||
' Log($"RDCConnector.GetPoolStats: CheckoutTimeout = ${checkoutTime}"$)
|
||||
|
||||
' Log($"RDCConnector.GetPoolStats: CheckoutTimeout = ${checkoutTime}"$) ' Log (descomentar si es necesario)
|
||||
|
||||
Catch
|
||||
' Log("RDCConnector.GetPoolStats: ERROR CRÍTICO al obtener estadísticas del pool: " & LastException.Message)
|
||||
' Si ocurre un error al obtener las estadísticas, se registra y se añade un mensaje de error al mapa.
|
||||
Log("RDCConnector.GetPoolStats: ERROR CRÍTICO al obtener estadísticas del pool: " & LastException.Message)
|
||||
stats.Put("Error", LastException.Message)
|
||||
End Try
|
||||
Else
|
||||
' Log("RDCConnector.GetPoolStats: ADVERTENCIA: Pool NO está inicializado. Retornando mapa con error.")
|
||||
' Si el pool no está inicializado, se registra una advertencia y se devuelve un mapa con un error.
|
||||
Log("RDCConnector.GetPoolStats: ADVERTENCIA: Pool NO está inicializado. Retornando mapa con error.")
|
||||
stats.Put("Error", "Pool de conexiones no inicializado para esta DB.")
|
||||
End If
|
||||
|
||||
' *** CORRECCIÓN: Usamos JSONGenerator para serializar el mapa a String para el Log ***
|
||||
Dim tempJsonGen As JSONGenerator ' Declaramos un JSONGenerator temporal
|
||||
tempJsonGen.Initialize(stats) ' Lo inicializamos con el mapa 'stats'
|
||||
' Log("--- RDCConnector.GetPoolStats finalizado. Retornando stats: " & tempJsonGen.ToString & " ---") ' Log de fin con JSON
|
||||
|
||||
|
||||
' Se utiliza JSONGenerator para serializar el mapa de estadísticas a String para el log,
|
||||
' lo que permite una visualización estructurada y fácil de leer.
|
||||
Dim tempJsonGen As JSONGenerator
|
||||
tempJsonGen.Initialize(stats)
|
||||
' Log("--- RDCConnector.GetPoolStats finalizado. Retornando stats: " & tempJsonGen.ToString & " ---") ' Log de fin (descomentar si es necesario)
|
||||
|
||||
Return stats
|
||||
End Sub
|
||||
|
||||
' *** NUEVA SUBRUTINA: Cierra el pool de conexiones de forma ordenada usando JavaObject ***
|
||||
' Este método es crucial para liberar los recursos de la base de datos
|
||||
' cuando un conector RDC ya no es necesario o va a ser reemplazado.
|
||||
' Este método es crucial para liberar los recursos de la base de datos cuando un conector RDC
|
||||
' ya no es necesario o va a ser reemplazado (por ejemplo, durante un "Hot-Swap" de configuración).
|
||||
Public Sub Close
|
||||
If pool <> Null And pool.IsInitialized Then
|
||||
' Log($"RDCConnector: Cerrando pool de conexiones."$)
|
||||
' Log($"RDCConnector.Close: Cerrando pool de conexiones."$) ' Log (descomentar si es necesario)
|
||||
' Convertimos el objeto pool de B4X a un JavaObject para poder llamar a su método 'close()'
|
||||
' que no está expuesto directamente en la envoltura de B4X.
|
||||
' que no está expuesto directamente en la envoltura de B4X, asegurando un cierre limpio de C3P0.
|
||||
Dim joPool As JavaObject = pool
|
||||
joPool.RunMethod("close", Null) ' Llamamos al método 'close()' del objeto Java subyacente de C3P0.
|
||||
End If
|
||||
|
||||
142
jRDC_Multi.b4j
142
jRDC_Multi.b4j
@@ -53,7 +53,7 @@ Version=10.3
|
||||
#Region Project Attributes
|
||||
#CommandLineArgs:
|
||||
#MergeLibraries: True
|
||||
' VERSION 5.09.14
|
||||
' VERSION 5.09.15
|
||||
'###########################################################################################################
|
||||
'###################### PULL #############################################################
|
||||
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull
|
||||
@@ -75,42 +75,68 @@ Version=10.3
|
||||
|
||||
Sub Process_Globals
|
||||
' --- Variables globales accesibles desde cualquier parte del proyecto ---
|
||||
Public srvr As Server ' El objeto principal del servidor HTTP de B4J.
|
||||
Public const VERSION As Float = 2.23 ' La versión actual de este servidor jRDC modificado.
|
||||
|
||||
' Tipos personalizados para la serialización y deserialización de datos
|
||||
|
||||
' Objeto principal del servidor HTTP de B4J.
|
||||
Public srvr As Server
|
||||
|
||||
' La versión actual de este servidor jRDC modificado.
|
||||
Public const VERSION As Float = 2.23
|
||||
|
||||
' Tipos personalizados (clases) para la serialización y deserialización de datos
|
||||
' entre el cliente B4X (DBRequestManager) y el servidor jRDC2.
|
||||
Type DBCommand (Name As String, Parameters() As Object) ' Define un comando SQL.
|
||||
Type DBResult (Tag As Object, Columns As Map, Rows As List) ' Define la estructura de un resultado de consulta.
|
||||
|
||||
Public listaDeCP As List ' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2").
|
||||
Private cpFiles As List ' Una lista temporal para almacenar los nombres de archivos encontrados en el directorio.
|
||||
|
||||
|
||||
' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2").
|
||||
Public listaDeCP As List
|
||||
|
||||
' Una lista temporal para almacenar los nombres de archivos de configuración encontrados.
|
||||
Private cpFiles As List
|
||||
|
||||
' Mapas globales para gestionar los conectores de base de datos y los comandos SQL.
|
||||
Public Connectors, commandsMap As Map ' Connectors: Almacena las instancias de RDCConnector por DB.
|
||||
' commandsMap: Almacena los comandos SQL cargados para cada DB.
|
||||
|
||||
Public SQL1 As SQL ' Objeto SQL para interactuar con la base de datos de usuarios (SQLite).
|
||||
Private bc As BCrypt ' Objeto para realizar operaciones de hashing de contraseñas de forma segura (para autenticación).
|
||||
Public MainConnectorsLock As JavaObject ' Objeto de bloqueo para proteger Main.Connectors
|
||||
' Connectors: Almacena las instancias de RDCConnector por cada base de datos (DB1, DB2, etc.).
|
||||
' commandsMap: Almacena los comandos SQL cargados de los archivos de configuración para cada DB.
|
||||
Public Connectors, commandsMap As Map
|
||||
|
||||
' Objeto SQL para interactuar con la base de datos de usuarios y logs (SQLite).
|
||||
Public SQL1 As SQL
|
||||
|
||||
' Objeto para realizar operaciones de hashing de contraseñas de forma segura (para autenticación de Manager).
|
||||
Private bc As BCrypt
|
||||
|
||||
' Objeto de bloqueo (ReentrantLock) para proteger el mapa Main.Connectors durante operaciones de recarga (Hot-Swap).
|
||||
Public MainConnectorsLock As JavaObject
|
||||
|
||||
' Mapa para contar las peticiones activas por cada base de datos. Es thread-safe.
|
||||
Public ActiveRequestsCountByDB As Map
|
||||
|
||||
' Timer para ejecutar tareas periódicas, como la limpieza de logs.
|
||||
Public timerLogs As Timer
|
||||
End Sub
|
||||
|
||||
Sub AppStart (Args() As String)
|
||||
' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
|
||||
bc.Initialize("BC")
|
||||
|
||||
' 1. Inicializa la base de datos local de usuarios (SQLite).
|
||||
' Esta base de datos se crea automáticamente si no existe y contiene los usuarios para el panel de administración.
|
||||
' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs.
|
||||
' Esta base de datos se crea automáticamente si no existe o se migra si es necesario.
|
||||
InitializeSQLiteDatabase
|
||||
|
||||
' <<<< Bloque de inicialización del Timer para la limpieza de logs >>>>
|
||||
' Inicializa y configura el Timer para borrar logs antiguos cada 10 minutos (600,000 milisegundos).
|
||||
timerLogs.Initialize("TimerLogs", 600000) ' 10 minutos = 600 * 1000 = 600000 ms
|
||||
timerLogs.Enabled = True ' Habilita el timer para que empiece a correr.
|
||||
Log("Main.AppStart: Timer de limpieza de 'query_logs' inicializado para ejecutarse cada 10 minutos.")
|
||||
' <<<< Fin del bloque del Timer >>>>
|
||||
|
||||
' 2. Inicializa los mapas globales definidos en GlobalParameters.bas.
|
||||
' Estos mapas se usan para monitorear el servidor y gestionar configuraciones dinámicas.
|
||||
GlobalParameters.mpLogs.Initialize ' Mapa para almacenar logs de actividad.
|
||||
GlobalParameters.mpLogs.Initialize ' Mapa para almacenar logs de actividad general.
|
||||
GlobalParameters.mpTotalRequests.Initialize ' Mapa para contar peticiones por endpoint/DB.
|
||||
GlobalParameters.mpTotalConnections.Initialize ' Mapa para almacenar el estado de los pools de conexión por DB.
|
||||
GlobalParameters.mpBlockConnection.Initialize ' Mapa para gestionar IPs bloqueadas (si la funcionalidad está activa).
|
||||
GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap ' Aseguramos que sea thread-safe para conteo de peticiones activas por DB [___new 3.txt, conversación]
|
||||
|
||||
' Aseguramos que el mapa de conteo de peticiones activas sea thread-safe para un manejo concurrente seguro.
|
||||
GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap
|
||||
|
||||
' 3. Inicializa las estructuras principales del servidor HTTP.
|
||||
listaDeCP.Initialize ' Inicializa la lista que contendrá los IDs de las bases de datos.
|
||||
@@ -118,9 +144,8 @@ Sub AppStart (Args() As String)
|
||||
Connectors = srvr.CreateThreadSafeMap ' Crea un mapa seguro para almacenar instancias de RDCConnector (un conector por DB).
|
||||
commandsMap.Initialize ' Inicializa el mapa que almacenará los comandos SQL cargados de los archivos de configuración.
|
||||
|
||||
' <<<< NUEVA INICIALIZACIÓN: Creamos una instancia de ReentrantLock para proteger Main.Connectors >>>>
|
||||
' Creamos una instancia de ReentrantLock para proteger Main.Connectors durante operaciones atómicas de Hot-Swap.
|
||||
MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null)
|
||||
' <<<< HASTA AQUÍ LA NUEVA INICIALIZACIÓN >>>>
|
||||
|
||||
' === 4. INICIALIZACIÓN DEL CONECTOR PARA LA BASE DE DATOS PRINCIPAL (DB1) ===
|
||||
' DB1 siempre usa el archivo 'config.properties' por defecto.
|
||||
@@ -128,7 +153,7 @@ Sub AppStart (Args() As String)
|
||||
con1.Initialize("DB1") ' Inicializa la instancia del conector para "DB1".
|
||||
Connectors.Put("DB1", con1) ' Asocia el identificador "DB1" con su instancia de RDCConnector.
|
||||
srvr.Port = con1.serverPort ' El puerto del servidor HTTP se obtiene del config.properties de DB1.
|
||||
listaDeCP.Add("DB1") ' Añade "DB1" a la lista de bases de datos gestionadas.]
|
||||
listaDeCP.Add("DB1") ' Añade "DB1" a la lista de bases de datos gestionadas.
|
||||
Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$)
|
||||
|
||||
' === 5. DETECCIÓN E INICIALIZACIÓN DE BASES DE DATOS ADICIONALES (DB2, DB3, DB4) ===
|
||||
@@ -145,7 +170,6 @@ Sub AppStart (Args() As String)
|
||||
listaDeCP.Add("DB2") ' Añade "DB2" a la lista de bases de datos.
|
||||
Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.")
|
||||
End If
|
||||
|
||||
' Procesa 'config.DB3.properties'
|
||||
If cpFiles.Get(i) = "config.DB3.properties" Then
|
||||
Dim con3 As RDCConnector ' Declara una variable específica y única para el conector de DB3.
|
||||
@@ -154,7 +178,6 @@ Sub AppStart (Args() As String)
|
||||
listaDeCP.Add("DB3") ' Añade "DB3" a la lista de bases de datos.
|
||||
Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.")
|
||||
End If
|
||||
|
||||
' Procesa 'config.DB4.properties'
|
||||
If cpFiles.Get(i) = "config.DB4.properties" Then
|
||||
Dim con4 As RDCConnector ' Declara una variable específica y única para el conector de DB4.
|
||||
@@ -183,16 +206,16 @@ Sub AppStart (Args() As String)
|
||||
' Asocia rutas URL específicas con clases que manejarán las peticiones correspondientes.
|
||||
' El último parámetro (True) indica que el handler se ejecutará en un nuevo hilo,
|
||||
' lo que es recomendable para la mayoría de los casos para evitar bloqueos.
|
||||
srvr.AddHandler("/ping", "ping", True) ' Endpoint simple para verificar si el servidor está activo.
|
||||
srvr.AddHandler("/test", "TestHandler", True) ' Endpoint para pruebas de conexión y estado del servidor.
|
||||
srvr.AddHandler("/login", "LoginHandler", True) ' Muestra la página HTML de login.
|
||||
srvr.AddHandler("/dologin", "DoLoginHandler", True) ' Procesa el intento de inicio de sesión.
|
||||
srvr.AddHandler("/logout", "LogoutHandler", True) ' Cierra la sesión del usuario.
|
||||
srvr.AddHandler("/changepass", "ChangePassHandler", True) ' Permite a los usuarios cambiar su contraseña.
|
||||
srvr.AddHandler("/manager", "Manager", True) ' Panel de administración del servidor (requiere autenticación).
|
||||
srvr.AddHandler("/ping", "ping", False) ' Endpoint simple para verificar si el servidor está activo.
|
||||
srvr.AddHandler("/test", "TestHandler", False) ' Endpoint para pruebas de conexión y estado del servidor.
|
||||
srvr.AddHandler("/login", "LoginHandler", False) ' Muestra la página HTML de login.
|
||||
srvr.AddHandler("/dologin", "DoLoginHandler", False) ' Procesa el intento de inicio de sesión.
|
||||
srvr.AddHandler("/logout", "LogoutHandler", False) ' Cierra la sesión del usuario.
|
||||
srvr.AddHandler("/changepass", "ChangePassHandler", False) ' Permite a los usuarios cambiar su contraseña.
|
||||
srvr.AddHandler("/manager", "Manager", False) ' Panel de administración del servidor (requiere autenticación).
|
||||
srvr.AddHandler("/DBJ", "DBHandlerJSON", False) ' Handler para clientes web (ej. JavaScript, Node.js) que usan JSON.
|
||||
srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) ' Un alias para el handler JSON, por si se usa en clientes específicos.
|
||||
srvr.AddHandler("/favicon.ico", "faviconHandler", True) ' Sirve el icono de la página (favicon).
|
||||
srvr.AddHandler("/favicon.ico", "faviconHandler", False) ' Sirve el icono de la página (favicon).
|
||||
srvr.AddHandler("/*", "DBHandlerB4X", False) ' Handler por defecto para clientes B4X (DBRequestManager),
|
||||
' procesa peticiones dinámicamente según la URL.
|
||||
|
||||
@@ -219,12 +242,13 @@ Sub InitializeSQLiteDatabase
|
||||
' Inicializa la conexión a la base de datos SQLite, creándola si no existe (último parámetro en True).
|
||||
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
|
||||
|
||||
' Define y ejecuta la sentencia SQL para crear la tabla 'users'.
|
||||
' Define y ejecuta la sentencia SQL para crear la tabla 'users' para la autenticación del Manager.
|
||||
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
|
||||
SQL1.ExecNonQuery(createUserTable)
|
||||
|
||||
' >>> INICIO: Creación de la tabla query_logs con las nuevas columnas desde CERO <<<
|
||||
Log("Creando tabla 'query_logs' con columnas de rendimiento.")
|
||||
' Esta tabla almacena métricas detalladas de cada query ejecutada.
|
||||
Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)"
|
||||
SQL1.ExecNonQuery(createQueryLogsTable)
|
||||
' >>> FIN: Creación de la tabla query_logs <<<
|
||||
@@ -244,20 +268,20 @@ Sub InitializeSQLiteDatabase
|
||||
|
||||
' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<<
|
||||
Log("Verificando y migrando tabla 'query_logs' si es necesario.")
|
||||
|
||||
|
||||
' Primero, verificar si la tabla query_logs existe.
|
||||
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then
|
||||
Log("Tabla 'query_logs' no encontrada, creándola con columnas de rendimiento.")
|
||||
Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)"
|
||||
SQL1.ExecNonQuery(createQueryLogsTable)
|
||||
Else
|
||||
' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes.
|
||||
' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes (busy_connections, handler_active_requests).
|
||||
Dim columnExists As Boolean
|
||||
Dim rs As ResultSet
|
||||
|
||||
|
||||
' --- VERIFICAR Y AÑADIR busy_connections ---
|
||||
columnExists = False
|
||||
' Ejecutamos PRAGMA sin WHERE y lo filtramos en código.
|
||||
' Ejecutamos PRAGMA para obtener la información de la tabla y verificar la existencia de la columna.
|
||||
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
|
||||
Do While rs.NextRow
|
||||
If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then
|
||||
@@ -265,7 +289,7 @@ Sub InitializeSQLiteDatabase
|
||||
Exit ' La columna ya existe, salimos del bucle.
|
||||
End If
|
||||
Loop
|
||||
rs.Close ' ¡Importante cerrar el ResultSet!
|
||||
rs.Close ' ¡Importante cerrar el ResultSet para liberar recursos!
|
||||
|
||||
If columnExists = False Then
|
||||
Log("Añadiendo columna 'busy_connections' a query_logs.")
|
||||
@@ -292,26 +316,34 @@ Sub InitializeSQLiteDatabase
|
||||
End If
|
||||
End Sub
|
||||
|
||||
' Subrutina para registrar las métricas de rendimiento de las queries
|
||||
' Subrutina para registrar las métricas de rendimiento de las queries en la tabla 'query_logs'.
|
||||
Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int)
|
||||
Try
|
||||
' El valor PoolBusyConnections ya se recibe directamente del handler.
|
||||
' Removemos la lógica anterior de obtenerlo del conector.
|
||||
' Dim connector As RDCConnector = Main.Connectors.Get(DbKey).As(RDCConnector)
|
||||
' Dim poolBusyConnections As Int = 0
|
||||
' If connector.IsInitialized Then
|
||||
' Dim poolStats As Map = connector.GetPoolStats
|
||||
' If poolStats.ContainsKey("BusyConnections") Then
|
||||
' poolBusyConnections = poolStats.Get("BusyConnections")
|
||||
' End If
|
||||
' Else
|
||||
' Log($"ADVERTENCIA: Conector RDC para ${DbKey} no inicializado al intentar loguear rendimiento."$)
|
||||
' End If
|
||||
|
||||
' Insertamos los datos en la tabla query_logs de SQLite
|
||||
' Los valores de PoolBusyConnections y HandlerActiveRequests ya se reciben directamente del handler,
|
||||
' eliminando la necesidad de obtenerlos del conector en este punto.
|
||||
|
||||
' Insertamos los datos en la tabla query_logs de SQLite.
|
||||
SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests) VALUES (?, ?, ?, ?, ?, ?, ?)", _
|
||||
Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests))
|
||||
Catch
|
||||
Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message)
|
||||
End Try
|
||||
End Sub
|
||||
End Sub
|
||||
|
||||
' Subrutina de evento para el Timer 'timerLogs'.
|
||||
' Se ejecuta periódicamente (cada 10 minutos) para limpiar la tabla de logs.
|
||||
Sub TimerLogs_Tick
|
||||
Try
|
||||
borraArribaDe15000Logs ' Llama a la función para limpiar los logs.
|
||||
Catch
|
||||
Log("ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
' Borra los registros más antiguos de la tabla 'query_logs', manteniendo solo los 15,000 más recientes.
|
||||
' Luego, optimiza el espacio de la base de datos SQLite con un 'vacuum'.
|
||||
Sub borraArribaDe15000Logs 'ignore
|
||||
Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.")
|
||||
SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )")
|
||||
SQL1.ExecNonQuery("vacuum;") ' Optimiza el espacio de almacenamiento de la base de datos.
|
||||
End Sub
|
||||
|
||||
@@ -40,6 +40,6 @@ ModuleClosedNodes6=
|
||||
ModuleClosedNodes7=
|
||||
ModuleClosedNodes8=
|
||||
ModuleClosedNodes9=
|
||||
NavigationStack=RDCConnector,GetCommand,164,0,RDCConnector,GetConnection,187,0,RDCConnector,GetPoolStats,273,0,RDCConnector,Close,283,0,DBHandlerJSON,Handle,94,5,DBHandlerJSON,CleanupAndLog,200,6,DBHandlerJSON,Initialize,10,0,DBHandlerJSON,Class_Globals,6,0,Cambios,Process_Globals,25,3,Main,AppStart,152,1
|
||||
NavigationStack=Main,Process_Globals,48,0,Manager,Class_Globals,9,0,Manager,Initialize,14,0,Manager,Handle0,463,0,Manager,Handle,157,4,faviconHandler,Handle,31,0,Main,AppStart,76,1,Main,TimerLogs_Tick,291,0,Main,LogQueryPerformance,281,0,Cambios,Process_Globals,41,0
|
||||
SelectedBuild=0
|
||||
VisibleModules=3,4,12,1,7,2,5
|
||||
VisibleModules=3,4,12,1,2,5,10,6
|
||||
|
||||
Reference in New Issue
Block a user