- 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:
2025-09-17 11:37:59 -06:00
parent 2ec8f5973f
commit 51c829b876
8 changed files with 905 additions and 699 deletions

View File

@@ -5,165 +5,179 @@ Type=StaticCode
Version=10.3 Version=10.3
@EndOfDesignText@ @EndOfDesignText@
' ######################################## ' ########################################
' ##### HISTORIAL DE CAMBIOS ##### ' ##### HISTORIAL DE CAMBIOS #####
' ######################################## ' ########################################
Sub Process_Globals 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 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 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, '- 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 una forma de probar con carga el servidor
'- Agregar la opcion de "Queries lentos" '- 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 ' -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. ' -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: ' -Problemas Identificados y Resueltos:
' -1. **Métricas de `BusyConnections` y `TotalConnections` inconsistentes o siempre en `0` en el `Manager` y `query_logs`:**
' -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.
' * **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.
' * **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.
' -2. **Contador `handler_active_requests` no decrementaba correctamente:** ' * **Solución**:
' * **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. ' * Se aseguró la declaración `Public ActiveRequestsCountByDB As Map` en `GlobalParameters.bas`.
' * **Solución**: ' * Se garantizó su inicialización como un `srvr.CreateThreadSafeMap` en `Main.AppStart` para un manejo concurrente seguro de los contadores.
' * Se aseguró la declaración `Public ActiveRequestsCountByDB As Map` en `GlobalParameters.bas`. ' * 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 garantizó su inicialización como un `srvr.CreateThreadSafeMap` en `Main.AppStart` para un manejo concurrente seguro de los contadores. ' * 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.
' * 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. ' * 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.
' * 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: ' -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. '- VERSION 5.09.13.3 (Ahora consolidado en 5.09.15)
' * **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
'- Implementación de "Hot-Swap" para recarga de configuraciones de DB sin reiniciar el servidor. '- Implementación de "Hot-Swap" para recarga de configuraciones de DB sin reiniciar el servidor.
'- Migración a ReentrantLock para sincronización debido a incompatibilidad con 'Sync'. '- Migración a ReentrantLock para sincronización debido a incompatibilidad con 'Sync'.
'- **Problemas Resueltos:** '- **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. '- 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 . '- 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". '- 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. '- 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:** '- **Cambios Implementados:**
'- **En `Main.bas`:** '- **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`. '- * **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. '- * **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`:** '- **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 . '- * **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`:** '- **En `Manager.bas`:**
'- * **Reemplazo completo de la lógica `If Command = "reload" Then`:** '- * **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` . '- * **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 . '- * **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 . '- * **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 . '- * **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 . '- * **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 . '- * **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 . '- * **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. '- • 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 '- VERSION 5.09.13.2 (Ahora consolidado en 5.09.15)
'- Módulo: DBHandlerJSON.bas '- Módulo: DBHandlerJSON.bas
'- Descripción de Cambios: Manejo de Peticiones POST con Content-Type: application/json '- 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 . '- • 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: '- • 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. '- 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). '- 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. '- 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 . '- 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 . '- 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. '- 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). '- • 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 '- VERSION 5.09.13 (Ahora consolidado en 5.09.15)
' feat: Mejora la inicialización del pool de conexiones y el soporte multi-DB. ' 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 ' **Problemas Resueltos:**
' del pool de conexiones (C3P0) para múltiples bases de datos y la depuración de logs '
' en el servidor jRDC2-Multi. ' 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.
' **Problemas Resueltos:** ' 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.
' '
' 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. ' **Cambios Implementados:**
' 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. ' **En `Main.bas`:**
' '
' **Cambios Implementados:** ' * **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 `Main.bas`:** ' **En `RDCConnector.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. ' * **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`:**
' **En `RDCConnector.bas`:** ' * **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.
' * **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. ' * **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.
' * **Reordenamiento y robustecimiento de `Initialize`:** ' * **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.
' * **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 '- VERSION 5.09.08
'- Se agregó que se puedan configurar en el config.properties los siguientes parametros: '- Se agregó que se puedan configurar en el config.properties los siguientes parametros:
' '- - setInitialPoolSize = 3
' - setInitialPoolSize = 3 '- - setMinPoolSize = 2
' - setMinPoolSize = 2 '- - setMaxPoolSize = 5
' - setMaxPoolSize = 5
'
'- Se agregaron en duro a RDConnector los siguientes parametros: '- Se agregaron en duro a RDConnector los siguientes parametros:
' '- - setMaxIdleTime <-- Tiempo máximo de inactividad de la conexión.
' - setMaxIdleTime <-- Tiempo máximo de inactividad de la conexión. '- - setMaxConnectionAge <-- Tiempo de vida máximo de una conexión.
' - setMaxConnectionAge <-- Tiempo de vida máximo de una conexión. '- - setCheckoutTimeout <-- Tiempo máximo de espera por 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 '- 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 '- 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. '- 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 '- VERSION 5.09.04
'- Se cambio el nombre del handler de B4X a DBHandlerB4X. '- Se cambio el nombre del handler de B4X a DBHandlerB4X.
'- Se quitaron los handlers que ya no servian. '- Se quitaron los handlers que ya no servian.
'- VERSION 5.09.01 '- VERSION 5.09.01
'- Se corrigieron errores en "Manager". '- Se corrigieron errores en "Manager".
'- Se cambiaron nombres de handlers. '- Se cambiaron nombres de handlers.
'- Se corrigio un error en la ruta de "www/login.html". '- Se corrigio un error en la ruta de "www/login.html".
'- VERSION 5.08.31 '- VERSION 5.08.31
'- Se corrigio que no avisaba cuando el query no requeria parametros y si se enviaban (en el JSONHandler) '- Se corrigio que no avisaba cuando el query no requeria parametros y si se enviaban (en el JSONHandler)
'- VERSION 5.08.30 '- VERSION 5.08.30
'- Se cambiaron los 4 handlers de B4A a uno solo que toma el DB de la ruta automáticamente. '- 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 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. '- Se modificó el Readme.md para incluir todos estos cambios.
'- VERSION 5.08.25 '- 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. '- 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 '- 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 '- 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 '- 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 agregó el parametro "setMaxPoolSize=5" para que solo genere 5 conexiones a la base de datos, antes generaba 15.
'- Se quitaron lineas previamente comentadas. '- Se quitaron lineas previamente comentadas.
'- VERSION 4.11.09 '- VERSION 4.11.09
'- Commit inicial on Nov 9, 2024 '- Commit inicial on Nov 9, 2024
End Sub
End Sub

View File

@@ -10,7 +10,7 @@ Sub Class_Globals
End Sub End Sub
Public Sub Initialize Public Sub Initialize
' bc.Initialize ' <<--- CORRECCIÓN 1: Descomentado para que el objeto se cree. bc.Initialize("BC")
End Sub End Sub
Public Sub Handle(req As ServletRequest, resp As ServletResponse) Public Sub Handle(req As ServletRequest, resp As ServletResponse)

View File

@@ -4,178 +4,230 @@ ModulesStructureVersion=1
Type=Class Type=Class
Version=10.3 Version=10.3
@EndOfDesignText@ @EndOfDesignText@
' Handler genérico para peticiones desde clientes B4A/B4i (DBRequestManager) ' Módulo de clase: DBHandlerB4X
' Determina la base de datos a utilizar dinámicamente a partir de la URL de la petición. ' Este handler genérico se encarga de procesar las peticiones HTTP provenientes
' Versión con validación de parámetros y errores en texto plano. ' de clientes B4A/B4i (que utilizan la librería DBRequestManager).
Sub Class_Globals ' La base de datos a utilizar (DB1, DB2, etc.) se determina dinámicamente
' Estas constantes y variables solo se compilan si se usa la #if VERSION1, ' a partir de la URL de la petición.
' lo que sugiere que es para dar soporte a una versión antigua del protocolo de comunicación. ' Esta versión incluye validaciones de parámetros y manejo de errores.
' #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 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 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 Private Connector As RDCConnector
End Sub 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 Public Sub Initialize
' Inicializa el mapa que asocia los códigos de tipo de columna de fecha/hora de JDBC ' 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") DateTimeMethods = CreateMap(91: "getDate", 92: "getTime", 93: "getTimestamp")
End Sub 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) 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 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 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 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 End If
Else Else
dbKey = "DB1" '[DBHandlerB4X.bas.txt, 51] ' Si la URL es solo "/", por defecto se usa "DB1".
dbKey = "DB1"
End If 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] ' Verifica si el dbKey extraído corresponde a una base de datos configurada y cargada en Main.
Dim ErrorMsg As String = $"Invalid DB key specified in URL: '${dbKey}'. Valid keys are: ${Main.listaDeCP}"$ '[DBHandlerB4X.bas.txt, 52] If Main.Connectors.ContainsKey(dbKey) = False Then
Log(ErrorMsg) '[DBHandlerB4X.bas.txt, 52] ' Si la base de datos no es válida, se construye un mensaje de error y se envía.
SendPlainTextError(resp, 400, ErrorMsg) '[DBHandlerB4X.bas.txt, 52] Dim ErrorMsg As String = $"Invalid DB key specified in URL: '${dbKey}'. Valid keys are: ${Main.listaDeCP}"$
' Aquí no se necesita CleanupAndLog, ya que el contador no se ha incrementado Log(ErrorMsg)
' y no se ha obtenido ninguna conexión del pool aún. SendPlainTextError(resp, 400, ErrorMsg) ' Envía una respuesta de error al cliente.
Return ' 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 End If
' === FIN DE LA LÓGICA DINÁMICA === ' === 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) --- ' --- INICIO: Conteo de peticiones activas para esta dbKey (Incrementar) ---
Dim currentActiveRequests As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0) '[___new 3.txt, 205] ' Este bloque incrementa un contador global que rastrea cuántas peticiones están
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentActiveRequests + 1) '[___new 3.txt, 205] ' activas para una base de datos específica en un momento dado.
Dim requestsBeforeDecrement As Int = currentActiveRequests + 1 '[___new 3.txt, 207] ' <<<< ¡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 --- ' --- 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 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 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. 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 --- Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler ---
Dim in As InputStream = req.InputStream '[DBHandlerB4X.bas.txt, 53] Dim in As InputStream = req.InputStream ' Obtiene el stream de entrada de la petición HTTP.
Dim method As String = req.GetParameter("method") '[DBHandlerB4X.bas.txt, 53] Dim method As String = req.GetParameter("method") ' Obtiene el parámetro 'method' de la URL (ej. "query2", "batch2").
Connector = Main.Connectors.Get(dbKey) '[DBHandlerB4X.bas.txt, 54] 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! >>>> ' <<<< ¡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 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 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
End If End If
' <<<< ¡FIN DE CAPTURA! >>>> ' <<<< ¡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 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. If q = "error" Then ' Si ExecuteQuery2 devolvió un error de validación.
duration = DateTime.Now - start duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return Return ' Salida temprana si hay un error.
End If End If
'#if VERSION1 #if VERSION1
Else if method = "query" Then ' Estas ramas se compilan solo si #if VERSION1 está activo (para protocolo antiguo).
in = cs.WrapInputStream(in, "gzip") Else if method = "query" Then
q = ExecuteQuery(dbKey, con, in, resp) '[DBHandlerB4X.bas.txt, 55] in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1.
If q = "error" Then q = ExecuteQuery(dbKey, con, in, resp)
duration = DateTime.Now - start If q = "error" Then
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) duration = DateTime.Now - start
Return CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
End If Return
Else if method = "batch" Then End If
in = cs.WrapInputStream(in, "gzip") Else if method = "batch" Then
q = ExecuteBatch(dbKey, con, in, resp) '[DBHandlerB4X.bas.txt, 55] in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1.
If q = "error" Then q = ExecuteBatch(dbKey, con, in, resp)
duration = DateTime.Now - start If q = "error" Then
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) duration = DateTime.Now - start
Return CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
End If Return
'#end if End If
#end if
Else if method = "batch2" Then 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 If q = "error" Then
duration = DateTime.Now - start duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return Return ' Salida temprana si hay un error.
End If End If
Else Else
Log("Unknown method: " & method) '[DBHandlerB4X.bas.txt, 56] ' Si el método solicitado no es reconocido, se registra un error y se envía una respuesta adecuada.
SendPlainTextError(resp, 500, "unknown method") '[DBHandlerB4X.bas.txt, 56] Log("Unknown method: " & method)
q = "unknown_method_handler" ' Aseguramos un valor para q en el log. SendPlainTextError(resp, 500, "unknown method")
q = "unknown_method_handler" ' Aseguramos un valor para 'q' para que el log sea informativo.
duration = DateTime.Now - start duration = DateTime.Now - start
CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return Return ' Salida temprana.
End If End If
Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL --- Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL ---
Log(LastException) '[DBHandlerB4X.bas.txt, 56] ' Si ocurre una excepción inesperada durante el procesamiento de la petición.
SendPlainTextError(resp, 500, LastException.Message) '[DBHandlerB4X.bas.txt, 56] Log(LastException) ' Registra la excepción completa en el log.
q = "error_in_b4x_handler" ' Aseguramos un valor para q en el log si hay excepción. 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 --- End Try ' --- FIN: Bloque Try principal ---
' --- Lógica de logging y limpieza final (para rutas de ejecución normal o después de Catch) --- ' --- 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] ' Este bloque se asegura de que, independientemente de cómo termine la petición (éxito o error),
Log($"Command: ${q}, took: ${duration}ms, client=${req.RemoteAddress}"$) '[DBHandlerB4X.bas.txt, 57] ' 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) CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
End Sub 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) 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. ' Log($"[DEBUG] CleanupAndLog Entry (B4X): dbKey=${dbKey}, handlerReqs=${handlerReqs}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
Main.LogQueryPerformance(qName, durMs, dbKey, clientIp, handlerReqs, poolBusyConns) '[___new 3.txt, 207] ' 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 If currentCount > 0 Then
' Si el contador es positivo, lo decrementamos.
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1) GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1)
Else 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."$) Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$)
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0) GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0)
End If 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! >>>> ' <<<< ¡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 If conn <> Null And conn.IsInitialized Then conn.Close
End Sub End Sub
' --- Subrutinas para manejar la ejecución de queries y batches (Protocolo V2) ---
' Ejecuta una consulta única usando el protocolo V2 (B4XSerializator). ' 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 ' DB: Identificador de la base de datos.
' Objeto para deserializar los datos enviados desde el cliente. ' con: La conexión SQL obtenida del pool.
Dim ser As B4XSerializator ' 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. ' 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)) 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") 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") Dim limit As Int = m.Get("limit")
' Obtiene la sentencia SQL correspondiente al nombre del comando desde config.properties. ' 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. ' Cuenta cuántos parámetros se recibieron.
Dim receivedParams As Int Dim receivedParams As Int
If cmd.Parameters = Null Then receivedParams = 0 Else receivedParams = cmd.Parameters.Length If cmd.Parameters = Null Then receivedParams = 0 Else receivedParams = cmd.Parameters.Length
' Compara el número de parámetros esperados con los recibidos. ' Compara el número de parámetros esperados con los recibidos.
If expectedParams <> receivedParams Then If expectedParams <> receivedParams Then
Dim errorMessage As String = $"Número de parametros equivocado para "${cmd.Name}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ 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. ' Ejecuta la consulta SQL con los parámetros proporcionados.
Dim rs As ResultSet = con.ExecQuery2(sqlCommand, cmd.Parameters) 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). ' 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 If limit <= 0 Then limit = 0x7fffffff 'max int
' Obtiene el objeto Java subyacente del ResultSet para acceder a métodos adicionales. ' Obtiene el objeto Java subyacente del ResultSet para acceder a métodos adicionales.
Dim jrs As JavaObject = rs Dim jrs As JavaObject = rs
' Obtiene los metadatos del ResultSet (información sobre las columnas). ' Obtiene los metadatos del ResultSet (información sobre las columnas).
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null)
' Obtiene el número de columnas del resultado. ' Obtiene el número de columnas del resultado.
Dim cols As Int = rs.ColumnCount 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.Initialize
res.columns.Initialize res.columns.Initialize
res.Tag = Null res.Tag = Null
' Llena el mapa de columnas con el nombre de cada columna y su índice. ' Llena el mapa de columnas con el nombre de cada columna y su índice.
For i = 0 To cols - 1 For i = 0 To cols - 1
res.columns.Put(rs.GetColumnName(i), i) res.columns.Put(rs.GetColumnName(i), i)
Next Next
' Inicializa la lista de filas. ' Inicializa la lista de filas.
res.Rows.Initialize res.Rows.Initialize
' Itera sobre cada fila del ResultSet, hasta llegar al límite. ' Itera sobre cada fila del ResultSet, hasta llegar al límite.
Do While rs.NextRow And limit > 0 Do While rs.NextRow And limit > 0
Dim row(cols) As Object Dim row(cols) As Object
@@ -267,27 +324,36 @@ Private Sub ExecuteQuery2 (DB As String, con As SQL, in As InputStream, resp As
Loop Loop
' Cierra el ResultSet para liberar recursos. ' Cierra el ResultSet para liberar recursos.
rs.Close rs.Close
' Serializa el objeto DBResult completo a un array de bytes. ' Serializa el objeto DBResult completo a un array de bytes.
Dim data() As Byte = ser.ConvertObjectToBytes(res) Dim data() As Byte = ser.ConvertObjectToBytes(res)
' Escribe los datos serializados en el stream de respuesta. ' Escribe los datos serializados en el stream de respuesta.
resp.OutputStream.WriteBytes(data, 0, data.Length) resp.OutputStream.WriteBytes(data, 0, data.Length)
' Devuelve el nombre del comando para el log. ' Devuelve el nombre del comando para el log.
Return "query: " & cmd.Name Return "query: " & cmd.Name
End Sub End Sub
' Ejecuta un lote de comandos (INSERT, UPDATE, DELETE) usando el protocolo V2. ' 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 Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
Dim ser As B4XSerializator Dim ser As B4XSerializator
' Deserializa el mapa que contiene la lista de comandos. ' Deserializa el mapa que contiene la lista de comandos.
Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in)) Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in))
' Obtiene la lista de objetos DBCommand. ' Obtiene la lista de objetos DBCommand.
Dim commands As List = m.Get("commands") Dim commands As List = m.Get("commands")
' Prepara un objeto DBResult para la respuesta (aunque para batch no devuelve datos, solo confirmación). ' Prepara un objeto DBResult para la respuesta (aunque para batch no devuelve datos, solo confirmación).
Dim res As DBResult Dim res As DBResult
res.Initialize res.Initialize
res.columns = CreateMap("AffectedRows (N/A)": 0) res.columns = CreateMap("AffectedRows (N/A)": 0) ' Columna simbólica.
res.Rows.Initialize res.Rows.Initialize
res.Tag = Null res.Tag = Null
Try Try
' Inicia una transacción. Todos los comandos del lote se ejecutarán como una unidad. ' Inicia una transacción. Todos los comandos del lote se ejecutarán como una unidad.
con.BeginTransaction 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 expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
Dim receivedParams As Int Dim receivedParams As Int
If cmd.Parameters = Null Then receivedParams = 0 Else receivedParams = cmd.Parameters.Length 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. ' Si el número de parámetros no coincide, deshace la transacción y envía error.
If expectedParams <> receivedParams Then If expectedParams <> receivedParams Then
con.Rollback 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
End If End If
' --- FIN VALIDACIÓN --- ' --- FIN VALIDACIÓN ---
' Ejecuta el comando (no es una consulta, no devuelve filas). con.ExecNonQuery2(sqlCommand, cmd.Parameters) ' Ejecuta el comando (no es una consulta, no devuelve filas).
con.ExecNonQuery2(sqlCommand, cmd.Parameters)
Next Next
' Añade una fila simbólica al resultado para indicar éxito.
res.Rows.Add(Array As Object(0)) res.Rows.Add(Array As Object(0)) ' Añade una fila simbólica al resultado para indicar éxito.
' Si todos los comandos se ejecutaron sin error, confirma la transacción. con.TransactionSuccessful ' Si todos los comandos se ejecutaron sin error, confirma la transacción.
con.TransactionSuccessful
Catch Catch
' Si cualquier comando falla, se captura el error. ' Si cualquier comando falla, se captura el error.
con.Rollback ' Se deshacen todos los cambios hechos en la transacción. con.Rollback ' Se deshacen todos los cambios hechos en la transacción.
Log(LastException) Log(LastException) ' Registra la excepción.
SendPlainTextError(resp, 500, LastException.Message) SendPlainTextError(resp, 500, LastException.Message) ' Envía un error 500 al cliente.
End Try End Try
' Serializa y envía la respuesta al cliente. ' Serializa y envía la respuesta al cliente.
Dim data() As Byte = ser.ConvertObjectToBytes(res) Dim data() As Byte = ser.ConvertObjectToBytes(res)
resp.OutputStream.WriteBytes(data, 0, data.Length) resp.OutputStream.WriteBytes(data, 0, data.Length)
' Devuelve un resumen para el log. ' Devuelve un resumen para el log.
Return $"batch (size=${commands.Size})"$ Return $"batch (size=${commands.Size})"$
End Sub End Sub
' Código compilado condicionalmente para el protocolo antiguo (V1). ' --- Subrutinas para manejar la ejecución de queries y batches (Protocolo V1 - Compilación Condicional) ---
'#if VERSION1 ' 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. ' 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 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. ' Lee y descarta la versión del cliente.
Dim clientVersion As Float = ReadObject(in) 'ignore Dim clientVersion As Float = ReadObject(in) 'ignore
' Lee cuántos comandos vienen en el lote. ' Lee cuántos comandos vienen en el lote.
Dim numberOfStatements As Int = ReadInt(in) Dim numberOfStatements As Int = ReadInt(in)
Dim res(numberOfStatements) As Int ' Array para resultados (aunque no se usa). 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)
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> Try
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then con.BeginTransaction
con.Rollback ' Itera para procesar cada comando del lote.
Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ For i = 0 To numberOfStatements - 1
Log(errorMessage) ' Lee el nombre del comando y la lista de parámetros usando el deserializador V1.
SendPlainTextError(resp, 400, errorMessage) Dim queryName As String = ReadObject(in)
Return "error" Dim params As List = ReadList(in)
End If Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
' <<< 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 ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>>
con.Rollback If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
Dim errorMessage As String = $"Número de parametros equivocado para "${queryName}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ con.Rollback ' Deshace la transacción si un comando es inválido.
Log(errorMessage) Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$
SendPlainTextError(resp, 400, errorMessage) Log(errorMessage)
Return "error" SendPlainTextError(resp, 400, errorMessage)
End If Return "error"
End If End If
' --- FIN VALIDACIÓN --- ' <<< FIN NUEVA VALIDACIÓN >>>
' Ejecuta el comando. ' --- INICIO VALIDACIÓN DE PARÁMETROS DENTRO DEL BATCH (V1) ---
con.ExecNonQuery2(sqlCommand, params) If sqlCommand.Contains("?") Or (params <> Null And params.Size > 0) Then
Next Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
' Confirma la transacción. Dim receivedParams As Int
con.TransactionSuccessful If params = Null Then receivedParams = 0 Else receivedParams = params.Size
' Comprime la salida antes de enviarla. If expectedParams <> receivedParams Then
Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") con.Rollback
' Escribe la respuesta usando el serializador V1. Dim errorMessage As String = $"Número de parametros equivocado para "${queryName}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$
WriteObject(Main.VERSION, out) Log(errorMessage)
WriteObject("batch", out) SendPlainTextError(resp, 400, errorMessage)
WriteInt(res.Length, out) Return "error"
For Each r As Int In res End If
WriteInt(r, out) End If
Next ' --- FIN VALIDACIÓN ---
out.Close
Catch con.ExecNonQuery2(sqlCommand, params) ' Ejecuta el comando.
con.Rollback Next
Log(LastException)
SendPlainTextError(resp, 500, LastException.Message) con.TransactionSuccessful ' Confirma la transacción.
End Try
Return $"batch (size=${numberOfStatements})"$ 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 End Sub
' Ejecuta una consulta única usando el protocolo V1. ' 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 Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
Log("====================== ExecuteQuery =====================") Log("====================== ExecuteQuery =====================")
' Deserializa los datos de la petición usando el protocolo V1. ' Deserializa los datos de la petición usando el protocolo V1.
Dim clientVersion As Float = ReadObject(in) 'ignore Dim clientVersion As Float = ReadObject(in) 'ignore
Dim queryName As String = ReadObject(in) Dim queryName As String = ReadObject(in)
Dim limit As Int = ReadInt(in) Dim limit As Int = ReadInt(in)
Dim params As List = ReadList(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 >>>
' --- INICIO VALIDACIÓN DE PARÁMETROS (V1) --- ' Obtiene la sentencia SQL.
If theSql.Contains("?") Or (params <> Null And params.Size > 0) Then Dim theSql As String = Connector.GetCommand(DB, queryName)
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 ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>>
Dim errorMessage As String = $"Número de parametros equivocado para "${queryName}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$ If theSql = Null Or theSql ="null" Or theSql.Trim = "" Then
Log(errorMessage) Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$
SendPlainTextError(resp, 400, errorMessage) Log(errorMessage)
Return "error" SendPlainTextError(resp, 400, errorMessage)
End If Return "error"
End If End If
' --- FIN VALIDACIÓN --- ' <<< FIN NUEVA VALIDACIÓN >>>
' Ejecuta la consulta. ' --- INICIO VALIDACIÓN DE PARÁMETROS (V1) ---
Dim rs As ResultSet = con.ExecQuery2(theSql, params) If theSql.Contains("?") Or (params <> Null And params.Size > 0) Then
If limit <= 0 Then limit = 0x7fffffff 'max int Dim expectedParams As Int = theSql.Length - theSql.Replace("?", "").Length
Dim jrs As JavaObject = rs Dim receivedParams As Int
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) If params = Null Then receivedParams = 0 Else receivedParams = params.Size
Dim cols As Int = rs.ColumnCount
' Comprime el stream de salida. If expectedParams <> receivedParams Then
Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") Dim errorMessage As String = $"Número de parametros equivocado para "${queryName}". Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$
' Escribe la cabecera de la respuesta V1. Log(errorMessage)
WriteObject(Main.VERSION, out) SendPlainTextError(resp, 400, errorMessage)
WriteObject("query", out) Return "error"
WriteInt(rs.ColumnCount, out) End If
' Escribe los nombres de las columnas. End If
For i = 0 To cols - 1 ' --- FIN VALIDACIÓN ---
WriteObject(rs.GetColumnName(i), out)
Next ' Ejecuta la consulta.
Dim rs As ResultSet = con.ExecQuery2(theSql, params)
' Itera sobre las filas del resultado.
Do While rs.NextRow And limit > 0 If limit <= 0 Then limit = 0x7fffffff 'max int
' Escribe un byte '1' para indicar que viene una fila.
WriteByte(1, out) Dim jrs As JavaObject = rs
' Itera sobre las columnas de la fila. Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null)
For i = 0 To cols - 1 Dim cols As Int = rs.ColumnCount
Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1))
' Maneja los tipos de datos binarios de forma especial. Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime el stream de salida.
If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then
WriteObject(rs.GetBlob2(i), out) ' Escribe la cabecera de la respuesta V1.
Else WriteObject(Main.VERSION, out)
' Escribe el valor de la columna. WriteObject("query", out)
WriteObject(jrs.RunMethod("getObject", Array(i + 1)), out) WriteInt(rs.ColumnCount, out)
End If
Next ' Escribe los nombres de las columnas.
limit = limit - 1 For i = 0 To cols - 1
Loop WriteObject(rs.GetColumnName(i), out)
' Escribe un byte '0' para indicar el fin de las filas. Next
WriteByte(0, out)
out.Close ' Itera sobre las filas del resultado.
rs.Close Do While rs.NextRow And limit > 0
WriteByte(1, out) ' Escribe un byte '1' para indicar que viene una fila.
Return "query: " & queryName ' 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 End Sub
' Escribe un único byte en el stream de salida. ' Escribe un único byte en el stream de salida.
Private Sub WriteByte(value As Byte, out As OutputStream) 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 End Sub
' Serializador principal para el protocolo V1. Escribe un objeto al stream. ' Serializador principal para el protocolo V1. Escribe un objeto al stream.
Private Sub WriteObject(o As Object, out As OutputStream) Private Sub WriteObject(o As Object, out As OutputStream)
Dim data() As Byte Dim data() As Byte
' Escribe un byte de tipo seguido de los datos. ' Escribe un byte de tipo seguido de los datos.
If o = Null Then If o = Null Then
out.WriteBytes(Array As Byte(T_NULL), 0, 1) out.WriteBytes(Array As Byte(T_NULL), 0, 1)
Else If o Is Short Then Else If o Is Short Then
out.WriteBytes(Array As Byte(T_SHORT), 0, 1) out.WriteBytes(Array As Byte(T_SHORT), 0, 1)
data = bc.ShortsToBytes(Array As Short(o)) data = bc.ShortsToBytes(Array As Short(o))
Else If o Is Int Then Else If o Is Int Then
out.WriteBytes(Array As Byte(T_INT), 0, 1) out.WriteBytes(Array As Byte(T_INT), 0, 1)
data = bc.IntsToBytes(Array As Int(o)) data = bc.IntsToBytes(Array As Int(o))
Else If o Is Float Then Else If o Is Float Then
out.WriteBytes(Array As Byte(T_FLOAT), 0, 1) out.WriteBytes(Array As Byte(T_FLOAT), 0, 1)
data = bc.FloatsToBytes(Array As Float(o)) data = bc.FloatsToBytes(Array As Float(o))
Else If o Is Double Then Else If o Is Double Then
out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1) out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1)
data = bc.DoublesToBytes(Array As Double(o)) data = bc.DoublesToBytes(Array As Double(o))
Else If o Is Long Then Else If o Is Long Then
out.WriteBytes(Array As Byte(T_LONG), 0, 1) out.WriteBytes(Array As Byte(T_LONG), 0, 1)
data = bc.LongsToBytes(Array As Long(o)) data = bc.LongsToBytes(Array As Long(o))
Else If o Is Boolean Then Else If o Is Boolean Then
out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1) out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1)
Dim b As Boolean = o Dim b As Boolean = o
Dim data(1) As Byte Dim data(1) As Byte
If b Then data(0) = 1 Else data(0) = 0 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) Else If GetType(o) = "[B" Then ' Si el objeto es un array de bytes (BLOB)
data = o data = o
out.WriteBytes(Array As Byte(T_BLOB), 0, 1) out.WriteBytes(Array As Byte(T_BLOB), 0, 1)
' Escribe la longitud de los datos antes de los datos mismos. ' Escribe la longitud de los datos antes de los datos mismos.
WriteInt(data.Length, out) WriteInt(data.Length, out)
Else ' Trata todo lo demás como un String Else ' Trata todo lo demás como un String
out.WriteBytes(Array As Byte(T_STRING), 0, 1) out.WriteBytes(Array As Byte(T_STRING), 0, 1)
data = bc.StringToBytes(o, "UTF8") data = bc.StringToBytes(o, "UTF8")
' Escribe la longitud del string antes del string. ' Escribe la longitud del string antes del string.
WriteInt(data.Length, out) WriteInt(data.Length, out)
End If End If
' Escribe los bytes del dato. ' Escribe los bytes del dato.
If data.Length > 0 Then out.WriteBytes(data, 0, data.Length) If data.Length > 0 Then out.WriteBytes(data, 0, data.Length)
End Sub End Sub
' Deserializador principal para el protocolo V1. Lee un objeto del stream. ' Deserializador principal para el protocolo V1. Lee un objeto del stream.
Private Sub ReadObject(In As InputStream) As Object Private Sub ReadObject(In As InputStream) As Object
' Lee el primer byte para determinar el tipo de dato. ' Lee el primer byte para determinar el tipo de dato.
Dim data(1) As Byte Dim data(1) As Byte
In.ReadBytes(data, 0, 1) In.ReadBytes(data, 0, 1)
Select data(0) Select data(0)
Case T_NULL Case T_NULL
Return Null Return Null
Case T_SHORT Case T_SHORT
Dim data(2) As Byte Dim data(2) As Byte
Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_INT Case T_INT
Dim data(4) As Byte Dim data(4) As Byte
Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_LONG Case T_LONG
Dim data(8) As Byte Dim data(8) As Byte
Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_FLOAT Case T_FLOAT
Dim data(4) As Byte Dim data(4) As Byte
Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_DOUBLE Case T_DOUBLE
Dim data(8) As Byte Dim data(8) As Byte
Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_BOOLEAN Case T_BOOLEAN
Dim b As Byte = ReadByte(In) Dim b As Byte = ReadByte(In)
Return b = 1 Return b = 1
Case T_BLOB Case T_BLOB
' Lee la longitud, luego lee esa cantidad de bytes. ' Lee la longitud, luego lee esa cantidad de bytes.
Dim len As Int = ReadInt(In) Dim len As Int = ReadInt(In)
Dim data(len) As Byte Dim data(len) As Byte
Return ReadBytesFully(In, data, data.Length) Return ReadBytesFully(In, data, data.Length)
Case Else ' T_STRING Case Else ' T_STRING
' Lee la longitud, luego lee esa cantidad de bytes y los convierte a string. ' Lee la longitud, luego lee esa cantidad de bytes y los convierte a string.
Dim len As Int = ReadInt(In) Dim len As Int = ReadInt(In)
Dim data(len) As Byte Dim data(len) As Byte
ReadBytesFully(In, data, data.Length) ReadBytesFully(In, data, data.Length)
Return BytesToString(data, 0, data.Length, "UTF8") Return BytesToString(data, 0, data.Length, "UTF8")
End Select End Select
End Sub End Sub
' Se asegura de leer exactamente la cantidad de bytes solicitada del stream. ' 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() Private Sub ReadBytesFully(In As InputStream, Data() As Byte, Len As Int) As Byte()
Dim count = 0, Read As Int Dim count = 0, Read As Int
' Sigue leyendo en un bucle hasta llenar el buffer, por si los datos llegan en partes. ' Sigue leyendo en un bucle hasta llenar el buffer, por si los datos llegan en partes.
Do While count < Len And Read > -1 Do While count < Len And Read > -1
Read = In.ReadBytes(Data, count, Len - count) Read = In.ReadBytes(Data, count, Len - count)
count = count + Read count = count + Read
Loop Loop
Return Data Return Data
End Sub End Sub
' Escribe un entero (4 bytes) en el stream. ' Escribe un entero (4 bytes) en el stream.
Private Sub WriteInt(i As Int, out As OutputStream) Private Sub WriteInt(i As Int, out As OutputStream)
Dim data() As Byte Dim data() As Byte
data = bc.IntsToBytes(Array As Int(i)) data = bc.IntsToBytes(Array As Int(i))
out.WriteBytes(data, 0, data.Length) out.WriteBytes(data, 0, data.Length)
End Sub End Sub
' Lee un entero (4 bytes) del stream. ' Lee un entero (4 bytes) del stream.
Private Sub ReadInt(In As InputStream) As Int Private Sub ReadInt(In As InputStream) As Int
Dim data(4) As Byte Dim data(4) As Byte
Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0)
End Sub End Sub
' Lee un solo byte del stream. ' Lee un solo byte del stream.
Private Sub ReadByte(In As InputStream) As Byte Private Sub ReadByte(In As InputStream) As Byte
Dim data(1) As Byte Dim data(1) As Byte
In.ReadBytes(data, 0, 1) In.ReadBytes(data, 0, 1)
Return data(0) Return data(0)
End Sub End Sub
' Lee una lista de objetos del stream (protocolo V1). ' Lee una lista de objetos del stream (protocolo V1).
Private Sub ReadList(in As InputStream) As List Private Sub ReadList(in As InputStream) As List
' Primero lee la cantidad de elementos en la lista. ' Primero lee la cantidad de elementos en la lista.
Dim len As Int = ReadInt(in) Dim len As Int = ReadInt(in)
Dim l1 As List Dim l1 As List
l1.Initialize l1.Initialize
' Luego lee cada objeto uno por uno y lo añade a la lista. ' Luego lee cada objeto uno por uno y lo añade a la lista.
For i = 0 To len - 1 For i = 0 To len - 1
l1.Add(ReadObject(in)) l1.Add(ReadObject(in))
Next Next
Return l1 Return l1
End Sub 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. ' 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. ' Esto evita la página de error HTML por defecto que genera resp.SendError.
' resp: El objeto ServletResponse para enviar la respuesta. ' 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). ' 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. ' 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) Private Sub SendPlainTextError(resp As ServletResponse, statusCode As Int, errorMessage As String)
Try Try
' Establece el código de estado HTTP (ej. 400, 500). ' Establece el código de estado HTTP (ej. 400, 500).
resp.Status = statusCode resp.Status = statusCode
' Define el tipo de contenido como texto plano, con codificación UTF-8 para soportar acentos. ' Define el tipo de contenido como texto plano, con codificación UTF-8 para soportar acentos.
resp.ContentType = "text/plain; charset=utf-8" resp.ContentType = "text/plain; charset=utf-8"
' Obtiene el OutputStream de la respuesta para escribir los datos directamente. ' Obtiene el OutputStream de la respuesta para escribir los datos directamente.
Dim out As OutputStream = resp.OutputStream Dim out As OutputStream = resp.OutputStream
' Convierte el mensaje de error a un array de bytes usando UTF-8. ' Convierte el mensaje de error a un array de bytes usando UTF-8.
Dim data() As Byte = errorMessage.GetBytes("UTF8") Dim data() As Byte = errorMessage.GetBytes("UTF8")
' Escribe los bytes en el stream de salida. ' Escribe los bytes en el stream de salida.
out.WriteBytes(data, 0, data.Length) out.WriteBytes(data, 0, data.Length)
' Cierra el stream para asegurar que todos los datos se envíen correctamente. ' Cierra el stream para asegurar que todos los datos se envíen correctamente.
out.Close out.Close
Catch 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. ' para que no se pierda la causa original del problema.
Log("Error sending plain text error response: " & LastException) Log("Error sending plain text error response: " & LastException)
End Try End Try
End Sub End Sub

View File

@@ -4,114 +4,134 @@ ModulesStructureVersion=1
Type=Class Type=Class
Version=10.3 Version=10.3
@EndOfDesignText@ @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 Sub Class_Globals
' Declara una variable privada para mantener una instancia del conector RDC. ' 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 Private Connector As RDCConnector
End Sub End Sub
' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase. ' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase.
Public Sub Initialize Public Sub Initialize
' No se requiere inicialización específica para esta clase en este momento.
End Sub End Sub
' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp).
' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp). ' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp).
Sub Handle(req As ServletRequest, resp As ServletResponse) Sub Handle(req As ServletRequest, resp As ServletResponse)
' --- Headers CORS (Cross-Origin Resource Sharing) --- ' --- Headers CORS (Cross-Origin Resource Sharing) ---
resp.SetHeader("Access-Control-Allow-Origin", "*") ' Estos encabezados son esenciales para permitir que aplicaciones web (clientes)
resp.SetHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") ' alojadas en diferentes dominios puedan comunicarse con este servidor.
resp.SetHeader("Access-Control-Allow-Headers", "Content-Type") 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 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 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 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 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 poolBusyConnectionsForLog As Int = 0 ' Contiene el número de conexiones ocupadas del pool.
Dim finalDbKey As String = "DB1" ' Identificador de la base de datos, con valor por defecto "DB1".
Dim finalDbKey As String = "DB1" Dim requestsBeforeDecrement As Int = 0 ' Contador de peticiones activas antes de decrementar, inicializado en 0.
Dim requestsBeforeDecrement As Int = 0 ' Se inicializa en 0.
Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler --- Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler ---
Dim jsonString As String 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 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 Is0 As InputStream = req.InputStream
Dim bytes() As Byte = Bit.InputStreamToBytes(Is0) Dim bytes() As Byte = Bit.InputStreamToBytes(Is0) ' Lee el cuerpo completo de la petición.
jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") ' Convierte los bytes a una cadena JSON.
Is0.Close Is0.Close ' Cierra explícitamente el InputStream para liberar recursos.
Else 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") jsonString = req.GetParameter("j")
End If 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 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.") SendErrorResponse(resp, 400, "Falta el parámetro 'j' en el URL o el cuerpo JSON en la petición.")
duration = DateTime.Now - start 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) CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana. Return ' Salida temprana si no hay JSON válido.
End If End If
Dim parser As JSONParser Dim parser As JSONParser
parser.Initialize(jsonString) parser.Initialize(jsonString) ' Inicializa el parser JSON con la cadena recibida.
Dim RootMap As Map = parser.NextObject Dim RootMap As Map = parser.NextObject ' Parsea el JSON a un objeto Map.
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]
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 If paramsList = Null Or paramsList.IsInitialized = False Then
paramsList.Initialize paramsList.Initialize ' Si no hay parámetros, inicializa una lista vacía.
End If End If
' <<<< ¡CORRECCIÓN CLAVE AQUÍ: RESOLVEMOS finalDbKey del JSON ANTES! >>>> ' <<<< ¡CORRECCIÓN CLAVE: RESOLVEMOS finalDbKey del JSON ANTES de usarla para los contadores! >>>>
If RootMap.Get("dbx") <> Null Then finalDbKey = RootMap.Get("dbx") '[___new 3.txt, 204] ' 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! >>>> ' <<<< ¡FIN DE CORRECCIÓN CLAVE! >>>>
' --- INICIO: Conteo de peticiones activas para esta finalDbKey (Incrementar) --- ' --- 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) Dim currentCountFromMap As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(finalDbKey, 0).As(Int)
GlobalParameters.ActiveRequestsCountByDB.Put(finalDbKey, currentCountFromMap + 1) GlobalParameters.ActiveRequestsCountByDB.Put(finalDbKey, currentCountFromMap + 1)
requestsBeforeDecrement = currentCountFromMap + 1 ' Este es el valor que se registra en query_logs ' requestsBeforeDecrement es el valor del contador justo después de que esta petición lo incrementa.
' Log($"[DEBUG] Handle Increment: dbKey=${finalDbKey}, currentCountFromMap=${currentCountFromMap}, requestsBeforeDecrement=${requestsBeforeDecrement}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) ' 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 --- ' --- 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 If Main.listaDeCP.IndexOf(finalDbKey) = -1 Then
SendErrorResponse(resp, 400, "Parámetro 'DB' inválido. El nombre '" & finalDbKey & "' no es válido.") SendErrorResponse(resp, 400, "Parámetro 'DB' inválido. El nombre '" & finalDbKey & "' no es válido.")
duration = DateTime.Now - start duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana. Return ' Salida temprana si la DB no es válida.
End If End If
con = Connector.GetConnection(finalDbKey) ' La conexión a la BD se obtiene aquí. con = Connector.GetConnection(finalDbKey) ' ¡La conexión a la BD se obtiene aquí del pool de conexiones!
' <<<< ¡AÑADIR ESTE RETRASO ARTIFICIAL PARA LA PRUEBA! >>>> ' <<<< ¡CAPTURAMOS BUSY_CONNECTIONS INMEDIATAMENTE DESPUÉS DE OBTENER LA CONEXIÓN! >>>>
' Esto forzará a C3P0 a mantener las conexiones ocupadas por más tiempo. ' Este bloque captura el número de conexiones actualmente ocupadas en el pool
' Si tienes 100 VUs, esto debería hacer que BusyConnections suba. ' *después* de que esta petición ha obtenido la suya.
' Sleep(100) ' Retraso artificial de 100ms para pruebas.
' Log($"[DEBUG - ${finalDbKey}] Retraso artificial de 500ms aplicado. Pool Stats (antes de exec): Busy=${Connector.GetPoolStats.GetDefault("BusyConnections",0).As(Int)}, Total=${Connector.GetPoolStats.GetDefault("TotalConnections",0).As(Int)}"$ )
' <<<< ¡FIN DEL RETRASO ARTIFICIAL! >>>>
' <<<< BUSY_CONNECTIONS YA SE CAPTURABA BIEN. LO MANTENEMOS. >>>>
If Connector.IsInitialized Then 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 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
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) 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 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}'."$ Dim errorMessage As String = $"El comando '${queryNameForLog}' no fue encontrado en el config.properties de '${finalDbKey}'."$
Log(errorMessage) Log(errorMessage)
@@ -121,8 +141,10 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Return ' Salida temprana. Return ' Salida temprana.
End If End If
' --- Lógica para ejecutar diferentes tipos de comandos basados en el parámetro 'execType' ---
If execType.ToLowerCase = "executequery" Then If execType.ToLowerCase = "executequery" Then
Dim rs As ResultSet Dim rs As ResultSet
' Validación de parámetros para ExecuteQuery.
If sqlCommand.Contains("?") Or paramsList.Size > 0 Then If sqlCommand.Contains("?") Or paramsList.Size > 0 Then
Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
Dim receivedParams As Int = paramsList.Size Dim receivedParams As Int = paramsList.Size
@@ -133,31 +155,32 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana. Return ' Salida temprana.
End If End If
rs = con.ExecQuery2(sqlCommand, paramsList) rs = con.ExecQuery2(sqlCommand, paramsList) ' Ejecuta la consulta con parámetros.
Else Else
rs = con.ExecQuery(sqlCommand) rs = con.ExecQuery(sqlCommand) ' Ejecuta la consulta sin parámetros.
End If End If
Dim ResultList As List Dim ResultList As List
ResultList.Initialize ResultList.Initialize ' Lista para almacenar los resultados de la consulta.
Dim jrs As JavaObject = rs Dim jrs As JavaObject = rs ' Objeto Java subyacente del ResultSet para metadatos.
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) ' Metadatos del ResultSet.
Dim cols As Int = rsmd.RunMethod("getColumnCount", Null) 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 Dim RowMap As Map
RowMap.Initialize RowMap.Initialize ' Mapa para almacenar los datos de la fila actual.
For i = 1 To cols For i = 1 To cols ' Itera sobre cada columna.
Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i)) Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i)) ' Nombre de la columna.
Dim value As Object = jrs.RunMethod("getObject", Array(i)) Dim value As Object = jrs.RunMethod("getObject", Array(i)) ' Valor de la columna.
RowMap.Put(ColumnName, value) RowMap.Put(ColumnName, value) ' Añade la columna y su valor al mapa de la fila.
Next Next
ResultList.Add(RowMap) ResultList.Add(RowMap) ' Añade el mapa de la fila a la lista de resultados.
Loop Loop
rs.Close rs.Close ' Cierra el ResultSet.
SendSuccessResponse(resp, CreateMap("result": ResultList)) SendSuccessResponse(resp, CreateMap("result": ResultList)) ' Envía la respuesta JSON de éxito.
Else If execType.ToLowerCase = "executecommand" Then Else If execType.ToLowerCase = "executecommand" Then
' Validación de parámetros para ExecuteCommand.
If sqlCommand.Contains("?") Then If sqlCommand.Contains("?") Then
Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
Dim receivedParams As Int = paramsList.Size Dim receivedParams As Int = paramsList.Size
@@ -168,58 +191,74 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Return ' Salida temprana. Return ' Salida temprana.
End If End If
End If End If
con.ExecNonQuery2(sqlCommand, paramsList) con.ExecNonQuery2(sqlCommand, paramsList) ' Ejecuta un comando (INSERT, UPDATE, DELETE).
SendSuccessResponse(resp, CreateMap("message": "Command executed successfully")) SendSuccessResponse(resp, CreateMap("message": "Command executed successfully")) ' Envía confirmación de éxito.
Else Else
' Si el tipo de ejecución no es reconocido.
SendErrorResponse(resp, 400, "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido.") SendErrorResponse(resp, 400, "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido.")
' El flujo continúa hasta la limpieza final si no hay un Return explícito. ' El flujo continúa hasta la limpieza final si no hay un Return explícito.
End If End If
Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL/JSON --- Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL/JSON ---
Log(LastException) ' Si ocurre una excepción inesperada durante el procesamiento de la petición.
SendErrorResponse(resp, 500, LastException.Message) 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. queryNameForLog = "error_processing_json" ' Para registrar que hubo un error en el log.
End Try ' --- FIN: Bloque Try principal --- End Try ' --- FIN: Bloque Try principal ---
' --- Lógica de logging y limpieza final (para rutas de ejecución normal o después de Catch) --- ' --- 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) CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
End Sub 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) 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}"$) ' Los logs de depuración para CleanupAndLog pueden ser descomentados para una depuración profunda.
' 1. Llama a la subrutina centralizada para registrar el rendimiento. ' Log($"[DEBUG] CleanupAndLog Entry (JSON): dbKey=${dbKey}, handlerReqs=${handlerReqs}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
Main.LogQueryPerformance(qName, durMs, dbKey, clientIp, handlerReqs, poolBusyConns) '[___new 3.txt, 207] ' 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) 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 If currentCount > 0 Then
' Si el contador es positivo, lo decrementamos.
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1) GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1)
Else 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."$) Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$)
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0) GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0)
End If 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! >>>> ' <<<< ¡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 If conn <> Null And conn.IsInitialized Then conn.Close
End Sub End Sub
' --- Subrutinas de ayuda para respuestas JSON --- ' --- Subrutinas de ayuda para respuestas JSON ---
' Construye y envía una respuesta JSON de éxito. ' 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) 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. ' Añade el campo "success": true al mapa de datos para indicar que todo salió bien.
dataMap.Put("success", True) dataMap.Put("success", True)
' Crea un generador de JSON. ' Crea un generador de JSON.
Dim jsonGenerator As JSONGenerator Dim jsonGenerator As JSONGenerator
jsonGenerator.Initialize(dataMap) jsonGenerator.Initialize(dataMap)
' Establece el tipo de contenido de la respuesta a "application/json". ' Establece el tipo de contenido de la respuesta a "application/json".
resp.ContentType = "application/json" resp.ContentType = "application/json"
' Escribe la cadena JSON generada en el cuerpo de la respuesta HTTP. ' 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 End Sub
' Construye y envía una respuesta JSON de error. ' 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) 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. ' 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. ' Crea un mapa con el estado de error y el mensaje.
Dim resMap As Map = CreateMap("success": False, "error": errorMessage) Dim resMap As Map = CreateMap("success": False, "error": errorMessage)
' Genera la cadena JSON a partir del mapa. ' Genera la cadena JSON a partir del mapa.
Dim jsonGenerator As JSONGenerator Dim jsonGenerator As JSONGenerator
jsonGenerator.Initialize(resMap) jsonGenerator.Initialize(resMap)
' Establece el código de estado HTTP (ej. 400 para error del cliente, 500 para error del servidor). ' Establece el código de estado HTTP (ej. 400 para error del cliente, 500 para error del servidor).
resp.Status = statusCode resp.Status = statusCode
' Establece el tipo de contenido y escribe la respuesta de error. ' Establece el tipo de contenido y escribe la respuesta de error.
resp.ContentType = "application/json" resp.ContentType = "application/json"
resp.Write(jsonGenerator.ToString) resp.Write(jsonGenerator.ToString)
End Sub End Sub

View File

@@ -4,25 +4,39 @@ ModulesStructureVersion=1
Type=Class Type=Class
Version=8.8 Version=8.8
@EndOfDesignText@ @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 Sub Class_Globals
' Objeto para generar respuestas JSON. Se utiliza para mostrar mapas de datos de forma legible.
Dim j As JSONGenerator 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 End Sub
' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase.
Public Sub Initialize Public Sub Initialize
' No se requiere inicialización específica para esta clase en este momento.
End Sub 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) 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 If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
resp.SendRedirect("/login") resp.SendRedirect("/login")
Return Return ' Termina la ejecución si no está autorizado.
End If End If
' Obtiene el comando solicitado de los parámetros de la URL (ej. "?command=reload").
Dim Command As String = req.GetParameter("command") 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}"$) Log($"Command: ${Command}"$)
' --- MANEJO ESPECIAL PARA SNAPSHOT --- ' --- MANEJO ESPECIAL PARA SNAPSHOT ---
@@ -46,9 +60,9 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
End If End If
' --- FIN DE MANEJO ESPECIAL --- ' --- FIN DE MANEJO ESPECIAL ---
' Para todos los demás comandos, construimos la página HTML ' Para todos los demás comandos, construimos la página HTML de respuesta.
resp.ContentType = "text/html" resp.ContentType = "text/html" ' Establece el tipo de contenido como HTML.
Dim sb As StringBuilder Dim sb As StringBuilder ' Usamos StringBuilder para construir eficientemente el HTML.
sb.Initialize sb.Initialize
' --- Estilos y JavaScript (igual que antes) --- ' --- 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("<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>") 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("<h1>Panel de Administración jRDC</h1>")
sb.Append($"Bienvenido, <b>${req.GetSession.GetAttribute("username")}</b><br>"$) sb.Append($"<p>Bienvenido, <strong>${req.GetSession.GetAttribute("username")}</strong></p>"$)
' 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>") ' --- Menú de Navegación del Manager ---
' sb.Append("<button onclick='toggleForm()'>Cambiar Contraseña</button>") ' 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("<div id='changePassForm' style='display:none;'>")
sb.Append("<h2>Cambiar Contraseña</h2><form action='/changepass' method='post'>") 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>") 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. ' 1. Crear un nuevo mapa temporal para almacenar los conectores recién inicializados.
Dim newConnectors As Map Dim newConnectors As Map
newConnectors.Initialize newConnectors.Initialize
' Guardamos una referencia al mapa de conectores actualmente activos. ' Guardamos una referencia al mapa de conectores actualmente activos.
Dim oldConnectors As Map Dim oldConnectors As Map
Dim reloadSuccessful As Boolean = True Dim reloadSuccessful As Boolean = True
' *** INICIO DEL BLOQUE CRÍTICO 1: Obtener oldConnectors con ReentrantLock *** ' *** INICIO DEL BLOQUE CRÍTICO 1: Obtener oldConnectors con ReentrantLock ***
Dim lock1Acquired As Boolean = False ' Bandera para saber si el bloqueo fue adquirido. Dim lock1Acquired As Boolean = False ' Bandera para saber si el bloqueo fue adquirido.
Try 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) 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. Return ' Salir del Handle ya que ocurrió un error crítico irrecuperable.
End If End If
' 2. Iterar sobre las bases de datos configuradas y crear *nuevas* instancias de RDCConnector. ' 2. Iterar sobre las bases de datos configuradas y crear *nuevas* instancias de RDCConnector.
For Each dbKey As String In Main.listaDeCP For Each dbKey As String In Main.listaDeCP
Try Try
@@ -129,14 +154,14 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Dim newPoolStats As Map = newRDC.GetPoolStats Dim newPoolStats As Map = newRDC.GetPoolStats
sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append("<br>" & CRLF) sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append("<br>" & CRLF)
Catch Catch
sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append("<br>" & CRLF) sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append("<br>" & CRLF)
reloadSuccessful = False reloadSuccessful = False
Exit ' Si uno falla, abortamos la recarga completa para evitar un estado inconsistente. Exit ' Si uno falla, abortamos la recarga completa para evitar un estado inconsistente.
End Try End Try
Next Next
sb.Append(sbTemp.ToString) ' Añadimos los logs acumulados de la inicialización al StringBuilder principal. sb.Append(sbTemp.ToString) ' Añadimos los logs acumulados de la inicialización al StringBuilder principal.
If reloadSuccessful Then If reloadSuccessful Then

View File

@@ -5,45 +5,61 @@ Type=Class
Version=4.19 Version=4.19
@EndOfDesignText@ @EndOfDesignText@
' Módulo de clase: RDCConnector ' Módulo de clase: RDCConnector
' Esta clase gestiona el pool de conexiones a una base de datos específica. ' Esta clase gestiona el pool de conexiones a una base de datos específica utilizando la librería C3P0.
' Cada instancia de RDCConnector maneja la conexión y los comandos para una base de datos. ' 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 Sub Class_Globals
Private pool As ConnectionPool ' Objeto principal para gestionar el pool de conexiones de la base de datos (usa C3P0 internamente). ' --- Variables globales de la clase ---
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. ' Objeto principal para gestionar el pool de conexiones de la base de datos (usa C3P0 internamente).
Public serverPort As Int ' El puerto que el servidor HTTP usará, obtenido del archivo de configuración principal (config.properties). Private pool As ConnectionPool
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. ' 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 End Sub
' Subrutina de inicialización para el conector de una base de datos específica. ' 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"). ' DB: El identificador único de la base de datos (ej. "DB1", "DB2").
Public Sub Initialize(DB As String) 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). ' 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 = "" If DB.EqualsIgnoreCase("DB1") Then DB = ""
' PASO 1: Cargar la configuración desde el archivo .properties correspondiente. ' 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) ' 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. ' para que la configuración cargada del archivo sea persistente para esta instancia del conector.
config = LoadConfigMap(DB) config = LoadConfigMap(DB)
' Bloque Try-Catch para la inicialización y configuración del pool. ' 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 Try
' PASO 2: Inicializar el objeto B4X ConnectionPool. ' PASO 2: Inicializar el objeto B4X ConnectionPool.
' Esto crea la instancia subyacente de com.mchange.v2.c3p0.ComboPooledDataSource (la librería C3P0). ' 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. ' 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")) 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. ' 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 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. ' 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. ' 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 initialPoolSize As Int = config.GetDefault("InitialPoolSize", 3)
Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2) 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) Dim acquireIncrement As Int = config.GetDefault("AcquireIncrement", 5)
' Configuración de los parámetros del pool de conexiones C3P0: ' 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("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("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("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("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("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("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). 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: ' 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. ' 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. ' 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! 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. ' 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) ' 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 ' *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. Dim tempCon As SQL = pool.GetConnection ' Adquiere una conexión para forzar al pool a inicializarse.
If tempCon.IsInitialized Then If tempCon.IsInitialized Then
tempCon.Close ' Devolvemos la conexión inmediatamente al pool para que esté disponible. tempCon.Close ' Devolvemos la conexión inmediatamente al pool para que esté disponible.
@@ -118,16 +134,16 @@ Public Sub Initialize(DB As String)
' userOverrides -> {}, ' userOverrides -> {},
' usesTraditionalReflectiveProxies -> False ' usesTraditionalReflectiveProxies -> False
' ] ' ]
' '
Catch Catch
' Si ocurre un error durante la inicialización del pool o al forzar la conexión, ' 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. ' 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}"$) Log($"RDCConnector.Initialize para ${DB}: ERROR CRÍTICO al inicializar/forzar conexión: ${LastException.Message}"$)
End Try End Try
' Configuración de depuración de queries. Se activa automáticamente si el proyecto se ejecuta en modo DEBUG. ' Configuración de depuración de queries. Se activa automáticamente si el proyecto se ejecuta en modo DEBUG.
#If DEBUG #If DEBUG
' DebugQueries = True ' DebugQueries = True ' Descomentar para activar la recarga de comandos en cada petición en desarrollo.
#Else #Else
DebugQueries = False DebugQueries = False
#End If #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. ' 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). ' Nota: En el diseño actual, el puerto principal lo define DB1 (config.properties).
serverPort = config.Get("ServerPort") serverPort = config.Get("ServerPort")
' Asegura que el identificador DB no sea una cadena vacía para la carga de comandos. ' 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" 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) LoadSQLCommands(config, DB)
End Sub End Sub
@@ -149,7 +166,7 @@ End Sub
Private Sub LoadConfigMap(DB As String) As Map Private Sub LoadConfigMap(DB As String) As Map
Private DBX As String = "" Private DBX As String = ""
If DB <> "" Then DBX = "." & DB ' Construye el sufijo del nombre de archivo (ej. ".DB2"). 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") Return File.ReadMap("./", "config" & DBX & ".properties")
End Sub End Sub
@@ -158,9 +175,10 @@ End Sub
' Key: El nombre del comando SQL (ej. "select_user"). ' Key: El nombre del comando SQL (ej. "select_user").
' Retorna la sentencia SQL como String. ' Retorna la sentencia SQL como String.
Public Sub GetCommand(DB As String, Key As String) As 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 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 End If
Return commands.Get("sql." & Key) ' Retorna la sentencia SQL. Return commands.Get("sql." & Key) ' Retorna la sentencia SQL.
End Sub End Sub
@@ -168,128 +186,131 @@ End Sub
' Obtiene una conexión SQL funcional del pool de conexiones para la base de datos especificada. ' Obtiene una conexión SQL funcional del pool de conexiones para la base de datos especificada.
' DB: El identificador de la base de datos. ' DB: El identificador de la base de datos.
' Retorna un objeto SQL (la conexión a la base de datos). ' Retorna un objeto SQL (la conexión a la base de datos).
'Public Sub GetConnection(DB As String) As SQL
' If DB.EqualsIgnoreCase("DB1") Then DB = ""
' ' En modo de depuración, recarga los comandos SQL en cada petición.
' ' Esto permite modificar queries en config.properties sin reiniciar el servidor.
' If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB)
' Return pool.GetConnection ' Retorna una conexión del pool.
'End Sub
Public Sub GetConnection(DB As String) As SQL Public Sub GetConnection(DB As String) As SQL
If DB.EqualsIgnoreCase("DB1") Then DB = "" If DB.EqualsIgnoreCase("DB1") Then DB = ""
' 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! >>>> ' <<<< 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..."$) ' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Solicitando conexión del pool..."$)
Dim conn As SQL = pool.GetConnection Dim conn As SQL = pool.GetConnection
' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Conexión obtenida. IsInitialized: ${conn.IsInitialized}"$) ' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Conexión obtenida. IsInitialized: ${conn.IsInitialized}"$)
If pool.IsInitialized Then ' Doble verificación del estado del pool para logging If pool.IsInitialized Then ' Doble verificación del estado del pool para logging más seguro
Dim jo As JavaObject = pool ' Dim jo As JavaObject = pool
' Aseguramos que los valores sean Ints, manejando posible retorno como Double. ' 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 busyCount As Int = jo.RunMethod("getNumBusyConnectionsAllUsers", Null).As(Object).As(Int)
Dim totalCount As Int = jo.RunMethod("getNumConnectionsAllUsers", 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}"$) ' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Estadísticas del Pool (después de obtener): Busy=${busyCount}, Total=${totalCount}"$)
End If End If
' <<<< ¡FIN DE LOS LOGS A BUSCAR! >>>> ' <<<< Fin del bloque de Logs de Depuración >>>>
Return conn Return conn ' Retorna una conexión del pool.
End Sub End Sub
' Carga todos los comandos SQL del mapa de configuración en el mapa global 'commandsMap'. ' Carga todos los comandos SQL del mapa de configuración en el mapa global 'commandsMap' de Main.
' config2: El mapa de configuración de la DB actual. ' config2: El mapa de configuración de la DB actual (JdbcUrl, User, Password, etc.).
' DB: El identificador de la base de datos. ' DB: El identificador de la base de datos.
Private Sub LoadSQLCommands(config2 As Map, DB As String) Private Sub LoadSQLCommands(config2 As Map, DB As String)
Dim newCommands As Map Dim newCommands As Map
newCommands.Initialize newCommands.Initialize
For Each k As String In config2.Keys For Each k As String In config2.Keys
If k.StartsWith("sql.") Then ' Busca claves que comiencen con "sql." (ej. "sql.select_user"). 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. newCommands.Put(k, config2.Get(k)) ' Añade el comando al mapa.
End If End If
Next Next
commands = newCommands ' Actualiza el mapa de comandos de esta instancia de RDCConnector. 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. Main.commandsMap.Put(DB, commands) ' Almacena el mapa de comandos en el mapa global 'commandsMap' de Main.
End Sub End Sub
' Nuevo: Obtiene estadísticas detalladas del pool de conexiones. ' 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 Public Sub GetPoolStats As Map
Dim stats As Map Dim stats As Map
stats.Initialize stats.Initialize
' Log("--- RDCConnector.GetPoolStats llamado ---") ' Log de inicio
' Log("--- RDCConnector.GetPoolStats llamado ---") ' Log de inicio (descomentar si es necesario)
If pool.IsInitialized Then If pool.IsInitialized Then
' Log("RDCConnector.GetPoolStats: Pool está inicializado. Intentando obtener métricas.") ' 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. Dim jo As JavaObject = pool ' Convertimos el objeto pool a JavaObject para acceder a sus métodos internos de C3P0.
Try Try
' --- Métricas en tiempo real del pool --- ' --- 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) Dim totalConn As Object = jo.RunMethod("getNumConnectionsAllUsers", Null)
stats.Put("TotalConnections", totalConn) 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) Dim busyConn As Object = jo.RunMethod("getNumBusyConnectionsAllUsers", Null)
stats.Put("BusyConnections", busyConn) 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) Dim idleConn As Object = jo.RunMethod("getNumIdleConnectionsAllUsers", Null)
stats.Put("IdleConnections", idleConn) 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) --- ' --- 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) Dim initialSize As Object = jo.RunMethod("getInitialPoolSize", Null)
stats.Put("InitialPoolSize", initialSize) 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) Dim minSize As Object = jo.RunMethod("getMinPoolSize", Null)
stats.Put("MinPoolSize", minSize) 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) Dim maxSize As Object = jo.RunMethod("getMaxPoolSize", Null)
stats.Put("MaxPoolSize", maxSize) 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) Dim acquireInc As Object = jo.RunMethod("getAcquireIncrement", Null)
stats.Put("AcquireIncrement", acquireInc) 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) Dim maxIdle As Object = jo.RunMethod("getMaxIdleTime", Null)
stats.Put("MaxIdleTime", maxIdle) 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) Dim maxAge As Object = jo.RunMethod("getMaxConnectionAge", Null)
stats.Put("MaxConnectionAge", maxAge) 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) Dim checkoutTime As Object = jo.RunMethod("getCheckoutTimeout", Null)
stats.Put("CheckoutTimeout", checkoutTime) stats.Put("CheckoutTimeout", checkoutTime)
' Log($"RDCConnector.GetPoolStats: CheckoutTimeout = ${checkoutTime}"$) ' Log($"RDCConnector.GetPoolStats: CheckoutTimeout = ${checkoutTime}"$) ' Log (descomentar si es necesario)
Catch 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) stats.Put("Error", LastException.Message)
End Try End Try
Else 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.") stats.Put("Error", "Pool de conexiones no inicializado para esta DB.")
End If End If
' *** CORRECCIÓN: Usamos JSONGenerator para serializar el mapa a String para el Log *** ' Se utiliza JSONGenerator para serializar el mapa de estadísticas a String para el log,
Dim tempJsonGen As JSONGenerator ' Declaramos un JSONGenerator temporal ' lo que permite una visualización estructurada y fácil de leer.
tempJsonGen.Initialize(stats) ' Lo inicializamos con el mapa 'stats' Dim tempJsonGen As JSONGenerator
' Log("--- RDCConnector.GetPoolStats finalizado. Retornando stats: " & tempJsonGen.ToString & " ---") ' Log de fin con JSON tempJsonGen.Initialize(stats)
' Log("--- RDCConnector.GetPoolStats finalizado. Retornando stats: " & tempJsonGen.ToString & " ---") ' Log de fin (descomentar si es necesario)
Return stats Return stats
End Sub End Sub
' *** NUEVA SUBRUTINA: Cierra el pool de conexiones de forma ordenada usando JavaObject *** ' *** 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 ' Este método es crucial para liberar los recursos de la base de datos cuando un conector RDC
' cuando un conector RDC ya no es necesario o va a ser reemplazado. ' ya no es necesario o va a ser reemplazado (por ejemplo, durante un "Hot-Swap" de configuración).
Public Sub Close Public Sub Close
If pool <> Null And pool.IsInitialized Then If pool <> Null And pool.IsInitialized Then
' Log($"RDCConnector: Cerrando pool de conexiones."$) ' Log($"RDCConnector.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()' ' 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 Dim joPool As JavaObject = pool
joPool.RunMethod("close", Null) ' Llamamos al método 'close()' del objeto Java subyacente de C3P0. joPool.RunMethod("close", Null) ' Llamamos al método 'close()' del objeto Java subyacente de C3P0.
End If End If

View File

@@ -53,7 +53,7 @@ Version=10.3
#Region Project Attributes #Region Project Attributes
#CommandLineArgs: #CommandLineArgs:
#MergeLibraries: True #MergeLibraries: True
' VERSION 5.09.14 ' VERSION 5.09.15
'########################################################################################################### '###########################################################################################################
'###################### PULL ############################################################# '###################### PULL #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull 'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull
@@ -75,42 +75,68 @@ Version=10.3
Sub Process_Globals Sub Process_Globals
' --- Variables globales accesibles desde cualquier parte del proyecto --- ' --- 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. ' Objeto principal del servidor HTTP de B4J.
Public srvr As Server
' Tipos personalizados para la serialización y deserialización de datos
' 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. ' entre el cliente B4X (DBRequestManager) y el servidor jRDC2.
Type DBCommand (Name As String, Parameters() As Object) ' Define un comando SQL. 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. 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"). ' 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. 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. ' 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. ' Connectors: Almacena las instancias de RDCConnector por cada base de datos (DB1, DB2, etc.).
' commandsMap: Almacena los comandos SQL cargados para cada DB. ' commandsMap: Almacena los comandos SQL cargados de los archivos de configuración para cada DB.
Public Connectors, commandsMap As Map
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). ' Objeto SQL para interactuar con la base de datos de usuarios y logs (SQLite).
Public MainConnectorsLock As JavaObject ' Objeto de bloqueo para proteger Main.Connectors 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 Public ActiveRequestsCountByDB As Map
' Timer para ejecutar tareas periódicas, como la limpieza de logs.
Public timerLogs As Timer
End Sub End Sub
Sub AppStart (Args() As String) Sub AppStart (Args() As String)
' --- Subrutina principal que se ejecuta al iniciar la aplicación --- ' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
bc.Initialize("BC")
' 1. Inicializa la base de datos local de usuarios (SQLite). ' 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 y contiene los usuarios para el panel de administración. ' Esta base de datos se crea automáticamente si no existe o se migra si es necesario.
InitializeSQLiteDatabase 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. ' 2. Inicializa los mapas globales definidos en GlobalParameters.bas.
' Estos mapas se usan para monitorear el servidor y gestionar configuraciones dinámicas. ' 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.mpTotalRequests.Initialize ' Mapa para contar peticiones por endpoint/DB.
GlobalParameters.mpTotalConnections.Initialize ' Mapa para almacenar el estado de los pools de conexión por DB. GlobalParameters.mpTotalConnections.Initialize ' Mapa para almacenar el estado de los pools de conexión por DB.
GlobalParameters.mpBlockConnection.Initialize ' Mapa para gestionar IPs bloqueadas (si la funcionalidad está activa). GlobalParameters.mpBlockConnection.Initialize ' Mapa para gestionar IPs bloqueadas (si la funcionalidad está activa).
GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap ' Aseguramos que sea thread-safe para conteo de peticiones activas por DB [___new 3.txt, conversación] ' 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. ' 3. Inicializa las estructuras principales del servidor HTTP.
listaDeCP.Initialize ' Inicializa la lista que contendrá los IDs de las bases de datos. listaDeCP.Initialize ' Inicializa la lista que contendrá los IDs de las bases de datos.
@@ -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). 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. 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) 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) === ' === 4. INICIALIZACIÓN DEL CONECTOR PARA LA BASE DE DATOS PRINCIPAL (DB1) ===
' DB1 siempre usa el archivo 'config.properties' por defecto. ' 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". con1.Initialize("DB1") ' Inicializa la instancia del conector para "DB1".
Connectors.Put("DB1", con1) ' Asocia el identificador "DB1" con su instancia de RDCConnector. 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. 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}"$) 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) === ' === 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. listaDeCP.Add("DB2") ' Añade "DB2" a la lista de bases de datos.
Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.") Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.")
End If End If
' Procesa 'config.DB3.properties' ' Procesa 'config.DB3.properties'
If cpFiles.Get(i) = "config.DB3.properties" Then If cpFiles.Get(i) = "config.DB3.properties" Then
Dim con3 As RDCConnector ' Declara una variable específica y única para el conector de DB3. Dim con3 As RDCConnector ' Declara una variable específica y única para el conector de DB3.
@@ -154,7 +178,6 @@ Sub AppStart (Args() As String)
listaDeCP.Add("DB3") ' Añade "DB3" a la lista de bases de datos. listaDeCP.Add("DB3") ' Añade "DB3" a la lista de bases de datos.
Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.") Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.")
End If End If
' Procesa 'config.DB4.properties' ' Procesa 'config.DB4.properties'
If cpFiles.Get(i) = "config.DB4.properties" Then If cpFiles.Get(i) = "config.DB4.properties" Then
Dim con4 As RDCConnector ' Declara una variable específica y única para el conector de DB4. Dim con4 As RDCConnector ' Declara una variable específica y única para el conector de DB4.
@@ -183,16 +206,16 @@ Sub AppStart (Args() As String)
' Asocia rutas URL específicas con clases que manejarán las peticiones correspondientes. ' Asocia rutas URL específicas con clases que manejarán las peticiones correspondientes.
' El último parámetro (True) indica que el handler se ejecutará en un nuevo hilo, ' El último parámetro (True) indica que el handler se ejecutará en un nuevo hilo,
' lo que es recomendable para la mayoría de los casos para evitar bloqueos. ' lo que es recomendable para la mayoría de los casos para evitar bloqueos.
srvr.AddHandler("/ping", "ping", True) ' Endpoint simple para verificar si el servidor está activo. srvr.AddHandler("/ping", "ping", False) ' Endpoint simple para verificar si el servidor está activo.
srvr.AddHandler("/test", "TestHandler", True) ' Endpoint para pruebas de conexión y estado del servidor. srvr.AddHandler("/test", "TestHandler", False) ' Endpoint para pruebas de conexión y estado del servidor.
srvr.AddHandler("/login", "LoginHandler", True) ' Muestra la página HTML de login. srvr.AddHandler("/login", "LoginHandler", False) ' Muestra la página HTML de login.
srvr.AddHandler("/dologin", "DoLoginHandler", True) ' Procesa el intento de inicio de sesión. srvr.AddHandler("/dologin", "DoLoginHandler", False) ' Procesa el intento de inicio de sesión.
srvr.AddHandler("/logout", "LogoutHandler", True) ' Cierra la sesión del usuario. srvr.AddHandler("/logout", "LogoutHandler", False) ' Cierra la sesión del usuario.
srvr.AddHandler("/changepass", "ChangePassHandler", True) ' Permite a los usuarios cambiar su contraseña. srvr.AddHandler("/changepass", "ChangePassHandler", False) ' Permite a los usuarios cambiar su contraseña.
srvr.AddHandler("/manager", "Manager", True) ' Panel de administración del servidor (requiere autenticación). srvr.AddHandler("/manager", "Manager", 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("/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("/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), srvr.AddHandler("/*", "DBHandlerB4X", False) ' Handler por defecto para clientes B4X (DBRequestManager),
' procesa peticiones dinámicamente según la URL. ' procesa peticiones dinámicamente según la URL.
@@ -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). ' Inicializa la conexión a la base de datos SQLite, creándola si no existe (último parámetro en True).
SQL1.InitializeSQLite(File.DirApp, dbFileName, True) SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
' Define y ejecuta la sentencia SQL para crear la tabla 'users'. ' 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)" Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
SQL1.ExecNonQuery(createUserTable) SQL1.ExecNonQuery(createUserTable)
' >>> INICIO: Creación de la tabla query_logs con las nuevas columnas desde CERO <<< ' >>> INICIO: Creación de la tabla query_logs con las nuevas columnas desde CERO <<<
Log("Creando tabla 'query_logs' con columnas de rendimiento.") 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)" 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) SQL1.ExecNonQuery(createQueryLogsTable)
' >>> FIN: Creación de la tabla query_logs <<< ' >>> 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 <<< ' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<<
Log("Verificando y migrando tabla 'query_logs' si es necesario.") Log("Verificando y migrando tabla 'query_logs' si es necesario.")
' Primero, verificar si la tabla query_logs existe. ' 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 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.") 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)" 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) SQL1.ExecNonQuery(createQueryLogsTable)
Else 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 columnExists As Boolean
Dim rs As ResultSet Dim rs As ResultSet
' --- VERIFICAR Y AÑADIR busy_connections --- ' --- VERIFICAR Y AÑADIR busy_connections ---
columnExists = False 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)") rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then
@@ -265,7 +289,7 @@ Sub InitializeSQLiteDatabase
Exit ' La columna ya existe, salimos del bucle. Exit ' La columna ya existe, salimos del bucle.
End If End If
Loop Loop
rs.Close ' ¡Importante cerrar el ResultSet! rs.Close ' ¡Importante cerrar el ResultSet para liberar recursos!
If columnExists = False Then If columnExists = False Then
Log("Añadiendo columna 'busy_connections' a query_logs.") Log("Añadiendo columna 'busy_connections' a query_logs.")
@@ -292,26 +316,34 @@ Sub InitializeSQLiteDatabase
End If End If
End Sub 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) Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int)
Try Try
' El valor PoolBusyConnections ya se recibe directamente del handler. ' Los valores de PoolBusyConnections y HandlerActiveRequests ya se reciben directamente del handler,
' Removemos la lógica anterior de obtenerlo del conector. ' eliminando la necesidad de obtenerlos del conector en este punto.
' Dim connector As RDCConnector = Main.Connectors.Get(DbKey).As(RDCConnector)
' Dim poolBusyConnections As Int = 0 ' Insertamos los datos en la tabla query_logs de SQLite.
' If connector.IsInitialized Then
' Dim poolStats As Map = connector.GetPoolStats
' If poolStats.ContainsKey("BusyConnections") Then
' poolBusyConnections = poolStats.Get("BusyConnections")
' End If
' Else
' Log($"ADVERTENCIA: Conector RDC para ${DbKey} no inicializado al intentar loguear rendimiento."$)
' End If
' Insertamos los datos en la tabla query_logs de SQLite
SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests) VALUES (?, ?, ?, ?, ?, ?, ?)", _ 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)) Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests))
Catch Catch
Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message) Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message)
End Try 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

View File

@@ -40,6 +40,6 @@ ModuleClosedNodes6=
ModuleClosedNodes7= ModuleClosedNodes7=
ModuleClosedNodes8= ModuleClosedNodes8=
ModuleClosedNodes9= 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 SelectedBuild=0
VisibleModules=3,4,12,1,7,2,5 VisibleModules=3,4,12,1,2,5,10,6