2 Commits

Author SHA1 Message Date
820fe9fc2b - VERSION 5.09.17
- fix(handlers, logs): Reporte robusto de AffectedRows (simbólico) y limpieza de tabla de errores
- Aborda dos problemas críticos para la estabilidad y fiabilidad del servidor: el manejo del conteo de filas afectadas en DMLs y la gestión del crecimiento de la tabla de logs de errores.

- Cambios Principales:

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

- Implementa la funcionalidad de cacheo de logs en memoria y escritura transaccional para reducir el overhead de E/S de disco en SQLite [1, 2].

- Cambios principales:
		1. Refactorización de LogQueryPerformance y LogServerError para que solo almacenen logs en las cachés globales (QueryLogCache y ErrorLogCache) [3].
		2. Introducción de WriteQueryLogsBatch y WriteErrorLogsBatch, que vacían las cachés y realizan la inserción a SQLite dentro de una única transacción atómica (`BeginTransaction`/`TransactionSuccessful`), disparada por umbral (`LOG_CACHE_THRESHOLD`) o periódicamente por `TimerLogs_Tick` [4-7].
		3. Corrección del manejo de objetos List en las rutinas de lote (Write*LogsBatch): Se implementó la copia explícita de contenido (`List.AddAll`) dentro del bloqueo (`MainConnectorsLock`) para asegurar que el lote mantenga sus registros, resolviendo el problema de tamaño cero causado por la asignación de referencias.
2025-09-19 18:43:55 -06:00
11 changed files with 882 additions and 661 deletions

View File

@@ -26,6 +26,30 @@ Sub Process_Globals
' - Que en el reporte de "Queries lentos" se pueda especificar de cuanto tiempo, ahorita esta de la ultima hora, pero que se pueda seleccionar desde una ' - Que en el reporte de "Queries lentos" se pueda especificar de cuanto tiempo, ahorita esta de la ultima hora, pero que se pueda seleccionar desde una
' lista, por ejemplo 15, 30, 45 y 60 minutos antes. ' lista, por ejemplo 15, 30, 45 y 60 minutos antes.
' - VERSION 5.09.17
' - fix(handlers, logs): Reporte robusto de AffectedRows (simbólico) y limpieza de tabla de errores
' - Aborda dos problemas críticos para la estabilidad y fiabilidad del servidor: el manejo del conteo de filas afectadas en DMLs y la gestión del crecimiento de la tabla de logs de errores.
' - Cambios Principales:
' 1. Fix AffectedRows (ExecuteBatch V1 y DBHandlerJSON): Dada la imposibilidad de capturar el conteo de filas afectadas real (Null) de forma segura o la falla total en tiempo de ejecución (Method: ExecNonQuery2 not matched) al usar reflexión, se revierte la lógica a la llamada directa de ExecNonQuery2. Si el comando DML se ejecuta sin lanzar una excepción SQL, se reporta simbólicamente '1' fila afectada al cliente (en el Protocolo V1 y en la respuesta JSON para executecommand) para confirmar el éxito de la operación.
' 2. Limpieza de Tabla de Errores: Se corrigió la subrutina Main.borraArribaDe15000Logs para incluir la tabla `errores` en la limpieza periódica. Esto asegura que el log de errores no crezca indefinidamente, manteniendo solo los 15,000 registros más recientes y realizando la optimización de espacio en disco con `vacuum`.
' - VERSION 5.09.16.2
' - feat(logs): Implementación de Cacheo y Escritura Transaccional en Lotes
'
' - Implementa la funcionalidad de cacheo de logs en memoria y escritura transaccional para reducir el overhead de E/S de disco en SQLite [1, 2].
'
' - Cambios principales:
' 1. Refactorización de LogQueryPerformance y LogServerError para que solo almacenen logs en las cachés globales (QueryLogCache y ErrorLogCache) [3].
' 2. Introducción de WriteQueryLogsBatch y WriteErrorLogsBatch, que vacían las cachés y realizan la inserción a SQLite dentro de una única transacción atómica (`BeginTransaction`/`TransactionSuccessful`), disparada por umbral (`LOG_CACHE_THRESHOLD`) o periódicamente por `TimerLogs_Tick` [4-7].
' 3. Corrección del manejo de objetos List en las rutinas de lote (Write*LogsBatch): Se implementó la copia explícita de contenido (`List.AddAll`) dentro del bloqueo (`MainConnectorsLock`) para asegurar que el lote mantenga sus registros, resolviendo el problema de tamaño cero causado por la asignación de referencias.
' - VERSION 5.09.16.1
' 1. Detalle de Comandos Batch: Se modificó DBHandlerB4X.bas (ExecuteBatch V1 y ExecuteBatch2 V2) para que, en lotes de tamaño 1, el Log retorne el nombre específico del comando (queryName) en lugar del genérico "batch (size=1)". Esto asegura que el query_logs registre la query exacta junto a su dbKey.
' 2. Timestamp Legible en SQLite: Se añade la columna timestamp_text_local a la tabla query_logs (incluyendo la lógica de migración en Main.InitializeSQLiteDatabase) y se actualiza Main.LogQueryPerformance para calcular e insertar el tiempo en formato yyyy/mm/dd HH:mm:ss.sss. Esto permite la inspección directa de la base de datos Sin necesidad de conversiones, mejorando la usabilidad para el análisis de rendimiento.
' - Versión: 5.09.16 ' - Versión: 5.09.16
' - feat: Implementa control de logs de SQLite granular por DBKey y corrige la concurrencia del Timer en Hot-Swap. ' - feat: Implementa control de logs de SQLite granular por DBKey y corrige la concurrencia del Timer en Hot-Swap.
' - Este commit introduce una mejora crucial en el rendimiento y la flexibilidad del servidor al permitir el control detallado del registro de logs en SQLite (users.db) por cada base de datos configurada (DB1, DB2, etc.). ' - Este commit introduce una mejora crucial en el rendimiento y la flexibilidad del servidor al permitir el control detallado del registro de logs en SQLite (users.db) por cada base de datos configurada (DB1, DB2, etc.).

View File

@@ -408,7 +408,17 @@ Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As S
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})"$
' Devuelve un resumen para el log, incluyendo el nombre de la query si es un lote de tamaño 1.
If commands.Size = 1 Then
' Obtenemos el único comando en el lote.
Dim cmd As DBCommand = commands.Get(0)
Return $"batch (size=1) - query: ${cmd.Name}"$
Else
' Si el lote es de tamaño > 1, mantenemos el resumen por tamaño.
Return $"batch (size=${commands.Size})"$ Return $"batch (size=${commands.Size})"$
End If
End Sub End Sub
' --- Subrutinas para manejar la ejecución de queries y batches (Protocolo V1 - Compilación Condicional) --- ' --- Subrutinas para manejar la ejecución de queries y batches (Protocolo V1 - Compilación Condicional) ---
@@ -417,21 +427,32 @@ End Sub
' 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
Log($"ExecuteBatch ${DB}"$)
' 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).
Dim singleQueryName As String = ""
Dim affectedCounts As List
Dim totalAffectedRows As Int
affectedCounts.Initialize
Try Try
con.BeginTransaction con.BeginTransaction
' Itera para procesar cada comando del lote. ' Itera para procesar cada comando del lote.
Log(numberOfStatements)
For i = 0 To numberOfStatements - 1 For i = 0 To numberOfStatements - 1
Log($"i: ${i}"$)
' Lee el nombre del comando y la lista de parámetros usando el deserializador V1. ' Lee el nombre del comando y la lista de parámetros usando el deserializador V1.
Dim queryName As String = ReadObject(in) Dim queryName As String = ReadObject(in)
Dim params As List = ReadList(in) Dim params As List = ReadList(in)
Log(params)
If numberOfStatements = 1 Then
singleQueryName = queryName 'Capturamos el nombre del query.
End If
Dim sqlCommand As String = Connector.GetCommand(DB, queryName) Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
Log(sqlCommand)
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>>
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
con.Rollback ' Deshace la transacción si un comando es inválido. con.Rollback ' Deshace la transacción si un comando es inválido.
@@ -452,18 +473,28 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se
Return "error" ' Salida temprana si la validación falla. Return "error" ' Salida temprana si la validación falla.
End If End If
Log(validationResult.ParamsToExecute)
Dim affectedCount As Int = 1 ' Asumimos éxito (1) ya que la llamada directa es la única que ejecuta el SQL sin fallar en runtime.
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta el comando con la lista de parámetros validada. con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta el comando con la lista de parámetros validada.
' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH (V1) >>> ' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH (V1) >>>
affectedCounts.Add(affectedCount) ' Añadimos el resultado (1) a la lista de respuesta V1
totalAffectedRows = totalAffectedRows + affectedCount ' Acumulamos el total para el log (aunque sea 1 simbólico)
Next Next
con.TransactionSuccessful ' Confirma la transacción. con.TransactionSuccessful ' Confirma la transacción.
Log("Transaction succesfull")
Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime la salida antes de enviarla. Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime la salida antes de enviarla.
' Escribe la respuesta usando el serializador V1. ' Escribe la respuesta usando el serializador V1.
WriteObject(Main.VERSION, out) WriteObject(Main.VERSION, out)
WriteObject("batch", out) WriteObject("batch", out)
WriteInt(res.Length, out) WriteInt(res.Length, out)
For Each r As Int In res Log(affectedCounts.Size)
For Each r As Int In affectedCounts
WriteInt(r, out) WriteInt(r, out)
Next Next
out.Close out.Close
@@ -475,7 +506,12 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se
SendPlainTextError(resp, 500, LastException.Message) SendPlainTextError(resp, 500, LastException.Message)
End Try End Try
' Return $"batch (size=${numberOfStatements})"$
If numberOfStatements = 1 And singleQueryName <> "" Then
Return $"batch (size=1) - query: ${singleQueryName}"$
Else
Return $"batch (size=${numberOfStatements})"$ Return $"batch (size=${numberOfStatements})"$
End If
End Sub End Sub
' Ejecuta una consulta única usando el protocolo V1. ' Ejecuta una consulta única usando el protocolo V1.

View File

@@ -44,6 +44,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
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" ' Identificador de la base de datos, con valor por defecto "DB1".
Dim requestsBeforeDecrement As Int = 0 ' Contador de peticiones activas antes de decrementar, inicializado en 0. Dim requestsBeforeDecrement As Int = 0 ' Contador de peticiones activas antes de decrementar, inicializado en 0.
Dim Total As Int = 0
Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler --- Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler ---
@@ -191,9 +192,9 @@ 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
Dim affectedCount As Int = 1 ' Asumimos éxito (1) si ExecNonQuery2 no lanza una excepción.
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta un comando con la lista de parámetros validada. con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta un comando con la lista de parámetros validada.
SendSuccessResponse(resp, CreateMap("message": "Command executed successfully")) ' Envía confirmación de éxito. SendSuccessResponse(resp, CreateMap("affectedRows": affectedCount, "message": "Command executed successfully")) ' Envía confirmación de éxito.
' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA --- ' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
Else Else
Dim ErrorMsg As String = "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido." Dim ErrorMsg As String = "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido."
@@ -201,7 +202,6 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
' El flujo continúa hasta la limpieza final si no hay un Return explícito. ' 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) ' Registra la excepción completa en el log. Log(LastException) ' Registra la excepción completa en el log.
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", LastException.Message, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log Main.LogServerError("ERROR", "DBHandlerJSON.Handle", LastException.Message, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
@@ -215,7 +215,6 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
duration = DateTime.Now - start ' Calcula la duración total de la petición. 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. ' 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 de rendimiento y la limpieza de recursos --- ' --- NUEVA SUBRUTINA: Centraliza el logging de rendimiento y la limpieza de recursos ---

View File

@@ -11,13 +11,18 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_MultiSALMA JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_Pruebas_Salma2
# Configuración del pool de conexiones # Define el número de conexiones que se intentarán crear al iniciar el pool.
InitialPoolSize=3 InitialPoolSize=1
MinPoolSize=2 # Fija el número mínimo de conexiones que el pool mantendrá abiertas.
MaxPoolSize=15 MinPoolSize=1
AcquireIncrement=2 # Define el número máximo de conexiones simultáneas.
MaxPoolSize=2
# Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
AcquireIncrement=1
# Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
MaxConnectionAge=60
# Configuración de tolerancia de parámetros: # Configuración de tolerancia de parámetros:
# 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos). # 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos).
@@ -25,6 +30,12 @@ AcquireIncrement=2
# Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto). # Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto).
parameterTolerance=1 parameterTolerance=1
# Configuración de los logs de SQLite:
# 1 = Habilita el registro de logs de queries y errores en la base de datos SQLite (users.db).
# 0 = Deshabilita el registro de logs de queries y errores en SQLite para optimizar el rendimiento.
# Por defecto, si no se especifica o el valor es diferente de 1, los logs estarán DESHABILITADOS.
enableSQLiteLogs=1
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
User=SALMA User=SALMA
Password=SALMAD2016M Password=SALMAD2016M

View File

@@ -11,13 +11,30 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_MultiSALMA JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_Pruebas_Salma3
# Configuración del pool de conexiones # Define el número de conexiones que se intentarán crear al iniciar el pool.
InitialPoolSize=3 InitialPoolSize=1
MinPoolSize=2 # Fija el número mínimo de conexiones que el pool mantendrá abiertas.
MaxPoolSize=15 MinPoolSize=1
AcquireIncrement=2 # Define el número máximo de conexiones simultáneas.
MaxPoolSize=2
# Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
AcquireIncrement=1
# Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
MaxConnectionAge=60
# Configuración de tolerancia de parámetros:
# 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos).
# 0 = Deshabilita la tolerancia (el servidor será estricto y lanzará un error si hay parámetros de más).
# Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto).
parameterTolerance=1
# Configuración de los logs de SQLite:
# 1 = Habilita el registro de logs de queries y errores en la base de datos SQLite (users.db).
# 0 = Deshabilita el registro de logs de queries y errores en SQLite para optimizar el rendimiento.
# Por defecto, si no se especifica o el valor es diferente de 1, los logs estarán DESHABILITADOS.
enableSQLiteLogs=0
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
User=SALMA User=SALMA

View File

@@ -11,13 +11,30 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_MultiSALMA JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_Pruebas_Salma4
# Configuración del pool de conexiones # Define el número de conexiones que se intentarán crear al iniciar el pool.
InitialPoolSize=3 InitialPoolSize=1
MinPoolSize=2 # Fija el número mínimo de conexiones que el pool mantendrá abiertas.
MaxPoolSize=15 MinPoolSize=1
AcquireIncrement=2 # Define el número máximo de conexiones simultáneas.
MaxPoolSize=2
# Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
AcquireIncrement=1
# Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
MaxConnectionAge=60
# Configuración de tolerancia de parámetros:
# 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos).
# 0 = Deshabilita la tolerancia (el servidor será estricto y lanzará un error si hay parámetros de más).
# Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto).
parameterTolerance=1
# Configuración de los logs de SQLite:
# 1 = Habilita el registro de logs de queries y errores en la base de datos SQLite (users.db).
# 0 = Deshabilita el registro de logs de queries y errores en SQLite para optimizar el rendimiento.
# Por defecto, si no se especifica o el valor es diferente de 1, los logs estarán DESHABILITADOS.
enableSQLiteLogs=0
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
User=SALMA User=SALMA

View File

@@ -11,13 +11,18 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.10:1521/DBKMT?v$session.program=jRDC_Multi JdbcUrl=jdbc:oracle:thin:@//192.168.101.10:1521/DBKMT?v$session.program=jRDC_Pruebas_Guna1
# Configuración del pool de conexiones # Define el número de conexiones que se intentarán crear al iniciar el pool.
InitialPoolSize=3 InitialPoolSize=1
MinPoolSize=2 # Fija el número mínimo de conexiones que el pool mantendrá abiertas.
MaxPoolSize=15 MinPoolSize=1
AcquireIncrement=2 # Define el número máximo de conexiones simultáneas.
MaxPoolSize=2
# Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
AcquireIncrement=1
# Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
MaxConnectionAge=60
# Configuración de tolerancia de parámetros: # Configuración de tolerancia de parámetros:
# 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos). # 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos).
@@ -25,6 +30,12 @@ AcquireIncrement=2
# Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto). # Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto).
parameterTolerance=1 parameterTolerance=1
# Configuración de los logs de SQLite:
# 1 = Habilita el registro de logs de queries y errores en la base de datos SQLite (users.db).
# 0 = Deshabilita el registro de logs de queries y errores en SQLite para optimizar el rendimiento.
# Por defecto, si no se especifica o el valor es diferente de 1, los logs estarán DESHABILITADOS.
enableSQLiteLogs=0
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
User=GUNA User=GUNA
Password=GUNAD2015M Password=GUNAD2015M

View File

@@ -2,7 +2,7 @@
Group=Default Group Group=Default Group
ModulesStructureVersion=1 ModulesStructureVersion=1
Type=Class Type=Class
Version=8.8 Version=10.3
@EndOfDesignText@ @EndOfDesignText@
' Módulo de clase: Manager ' Módulo de clase: Manager
' Este handler proporciona un panel de administración web para el servidor jRDC2-Multi. ' Este handler proporciona un panel de administración web para el servidor jRDC2-Multi.
@@ -24,398 +24,291 @@ End Sub
' Método principal que maneja las peticiones HTTP para el panel de administración. ' 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. ' 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. ' resp: El objeto ServletResponse para construir y enviar la respuesta al cliente.
' Módulo de clase: Manager
' ... (tu código de Class_Globals e Initialize se queda igual) ...
' Método principal que maneja las peticiones HTTP para el panel de administración.
' Refactorizado para funcionar como una API con un frontend estático.
Sub Handle(req As ServletRequest, resp As ServletResponse) Sub Handle(req As ServletRequest, resp As ServletResponse)
' --- 1. Bloque de Seguridad: Autenticación de Usuario --- ' --- 1. Bloque de Seguridad (sin cambios) ---
' 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 ' Termina la ejecución si no está autorizado. Return
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" ' Si no se especifica un comando, por defecto es "ping".
Log($"Command: ${Command}"$) ' --- 2. Servidor de la Página Principal ---
' Si NO se especifica un comando, servimos la página principal del manager desde la carpeta 'www'.
' --- MANEJO ESPECIAL PARA SNAPSHOT --- If Command = "" Then
' El comando "snapshot" no devuelve HTML, sino una imagen. Lo manejamos por separado al principio. Try
If Command = "snapshot" Then resp.ContentType = "text/html; charset=utf-8"
' Try resp.Write(File.ReadString(File.DirApp, "www/manager.html"))
' resp.ContentType = "image/png" Catch
' Dim robot, toolkit As JavaObject resp.SendError(500, "Error: No se pudo encontrar el archivo principal del panel (www/manager.html). " & LastException.Message)
' robot.InitializeNewInstance("java.awt.Robot", Null) End Try
' toolkit.InitializeStatic("java.awt.Toolkit") Return
' Dim screenRect As JavaObject
' screenRect.InitializeNewInstance("java.awt.Rectangle", Array As Object( _
' toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null)))
' Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(screenRect))
' Dim ImageIO As JavaObject
' ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object(image, "png", resp.OutputStream))
' Catch
' resp.SendError(500, LastException.Message)
' End Try
' Return ' Detenemos la ejecución aquí para no enviar más HTML.
End If End If
' --- FIN DE MANEJO ESPECIAL ---
' Para todos los demás comandos, construimos la página HTML de respuesta. ' --- 3. Manejo de Comandos como API ---
resp.ContentType = "text/html" ' Establece el tipo de contenido como HTML. ' La variable 'j' (JSONGenerator) está en Class_Globals
Dim sb As StringBuilder ' Usamos StringBuilder para construir eficientemente el HTML.
sb.Initialize
' --- Estilos y JavaScript (igual que antes) --- Select Command.ToLowerCase
sb.Append("<html><head><style>")
sb.Append("body {font-family: sans-serif; margin: 2em; background-color: #f9f9f9;} ")
sb.Append("h1, h2 {color: #333;} hr {margin: 2em 0; border: 0; border-top: 1px solid #ddd;} ")
sb.Append("input {display: block; width: 95%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px;} ")
sb.Append("button {padding: 10px 15px; border: none; background-color: #007bff; color: white; cursor: pointer; border-radius: 4px; margin-right: 1em;} ")
sb.Append(".nav a, .logout a {text-decoration: none; margin-right: 10px; color: #007bff;} ")
sb.Append(".output {background: #fff; padding: 1em; border: 1px solid #eee; border-radius: 8px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word;} ")
sb.Append("#changePassForm {background: #f0f0f0; padding: 1.5em; border-radius: 8px; max-width: 400px; margin-top: 1em;} ")
sb.Append("</style>")
sb.Append("<script>function toggleForm() {var form = document.getElementById('changePassForm'); if (form.style.display === 'none') {form.style.display = 'block';} else {form.style.display = 'none';}}</script>")
sb.Append("</head><body>")
' --- Cabecera de la Página y Mensaje de Bienvenida --- ' --- Comandos que devuelven JSON ---
sb.Append("<h1>Panel de Administración jRDC</h1>") Case "getstats"
sb.Append($"<p>Bienvenido, <strong>${req.GetSession.GetAttribute("username")}</strong></p>"$) resp.ContentType = "application/json; charset=utf-8"
Dim allPoolStats As Map
allPoolStats.Initialize
' --- Menú de Navegación del Manager --- For Each dbKey As String In Main.listaDeCP
' Este menú incluye las opciones para interactuar con el servidor. Dim connector As RDCConnector = Main.Connectors.Get(dbKey)
sb.Append("<div class='menu'>") If connector.IsInitialized Then
sb.Append("<a href='/manager?command=test'>Test</a> | ") allPoolStats.Put(dbKey, connector.GetPoolStats)
sb.Append("<a href='/manager?command=ping'>Ping</a> | ") Else
sb.Append("<a href='/manager?command=reload'>Reload</a> | ") allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
sb.Append("<a href='/manager?command=slowqueries'>Queries Lentas</a> | ") ' Nuevo enlace para queries lentas. End If
sb.Append("<a href='/manager?command=totalcon'>Estadísticas Pool</a> | ") ' Nuevo enlace para estadísticas del pool. Next
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;'>") j.Initialize(allPoolStats)
sb.Append("<h2>Cambiar Contraseña</h2><form action='/changepass' method='post'>") resp.Write(j.ToString)
sb.Append("Contraseña Actual: <input type='password' name='current_password' required><br>") Return
sb.Append("Nueva Contraseña: <input type='password' name='new_password' required><br>")
sb.Append("Confirmar Nueva Contraseña: <input type='password' name='confirm_password' required><br>")
sb.Append("<button type='submit'>Actualizar Contraseña</button> <button onclick='toggleForm()'>Cancelar</button></form></div>")
' --- Resultado del Comando --- Case "slowqueries"
sb.Append("<h2>Resultado del Comando: '" & Command & "'</h2>") resp.ContentType = "application/json; charset=utf-8"
sb.Append("<div class='output'>") Dim results As List
results.Initialize
Try
' Verificamos si la tabla de logs existe antes de consultarla
Dim tableExists As Boolean = Main.SQL1.ExecQuerySingleResult($"SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs';"$) <> Null
If tableExists = False Then
' Si la tabla no existe, devolvemos un JSON con un mensaje claro y terminamos.
j.Initialize(CreateMap("message": "La tabla de logs ('query_logs') no existe. Habilita 'enableSQLiteLogs=1' en la configuración."))
resp.Write(j.ToString)
Return
End If
' ========================================================================= ' La tabla existe, procedemos con la consulta original
' ### INICIO DE TU LÓGICA DE COMANDOS INTEGRADA ### Dim oneHourAgoMs As Long = DateTime.Now - 3600000
' ========================================================================= Dim rs As ResultSet = Main.SQL1.ExecQuery($"SELECT query_name, duration_ms, datetime(timestamp / 1000, 'unixepoch', 'localtime') as timestamp_local, db_key, client_ip, busy_connections, handler_active_requests FROM query_logs WHERE timestamp >= ${oneHourAgoMs} ORDER BY duration_ms DESC LIMIT 20"$)
If Command = "reload" Then
Do While rs.NextRow
Dim row As Map
row.Initialize
row.Put("Query", rs.GetString("query_name"))
row.Put("Duracion_ms", rs.GetLong("duration_ms"))
row.Put("Fecha_Hora", rs.GetString("timestamp_local"))
row.Put("DB_Key", rs.GetString("db_key"))
row.Put("Cliente_IP", rs.GetString("client_ip"))
row.Put("Conexiones_Ocupadas", rs.GetInt("busy_connections"))
row.Put("Peticiones_Activas", rs.GetInt("handler_active_requests"))
results.Add(row)
Loop
rs.Close
' 1. Creamos un mapa "raíz" para contener nuestra lista.
Dim root As Map
root.Initialize
root.Put("data", results) ' La llave puede ser lo que quieras, "data" es común.
' 2. Ahora sí, inicializamos el generador con el mapa raíz.
j.Initialize(root)
resp.Write(j.ToString)
Catch
Log("Error CRÍTICO al obtener queries lentas en Manager API: " & LastException.Message)
' <<< CORRECCIÓN AQUÍ >>>
' Se utiliza la propiedad .Status para asignar el código de error
resp.Status = 500 ' Internal Server Error
' 1. Creamos un mapa "raíz" para contener nuestra lista.
Dim root As Map
root.Initialize
root.Put("data", results) ' La llave puede ser lo que quieras, "data" es común.
' 2. Ahora sí, inicializamos el generador con el mapa raíz.
j.Initialize(root)
resp.Write(j.ToString)
End Try
Return
Case "logs", "totalrequests", "totalblocked"
resp.ContentType = "application/json; charset=utf-8"
Dim mp As Map
If Command = "logs" And GlobalParameters.mpLogs.IsInitialized Then mp = GlobalParameters.mpLogs
If Command = "totalrequests" And GlobalParameters.mpTotalRequests.IsInitialized Then mp = GlobalParameters.mpTotalRequests
If Command = "totalblocked" And GlobalParameters.mpBlockConnection.IsInitialized Then mp = GlobalParameters.mpBlockConnection
If mp.IsInitialized Then
j.Initialize(mp)
resp.Write(j.ToString)
Else
resp.Write("{}")
End If
Return
' --- Comandos que devuelven TEXTO PLANO ---
Case "ping"
resp.ContentType = "text/plain"
resp.Write($"Pong ($DateTime{DateTime.Now})"$)
Return
Case "reload"
resp.ContentType = "text/plain; charset=utf-8"
Dim sbTemp As StringBuilder Dim sbTemp As StringBuilder
sbTemp.Initialize sbTemp.Initialize
sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF)
' <<< PASO CLAVE 1: DETENER TIMER DE LOGS (ZONA SEGURA DE SQLite) >>> ' <<< LÓGICA ORIGINAL: Se mantiene intacta toda la lógica de recarga >>>
' Detenemos el timer incondicionalmente al inicio para evitar conflictos de bloqueo con SQLite ' (Copiada y pegada directamente de tu código anterior)
' durante la inicialización de conectores. sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF)
Dim oldTimerState As Boolean = Main.timerLogs.Enabled Dim oldTimerState As Boolean = Main.timerLogs.Enabled
If oldTimerState Then If oldTimerState Then
Main.timerLogs.Enabled = False Main.timerLogs.Enabled = False
sbTemp.Append(" -> Timer de limpieza de logs (SQLite) detenido temporalmente.").Append(" " & CRLF) sbTemp.Append(" -> Timer de limpieza de logs (SQLite) detenido temporalmente.").Append(" " & CRLF)
End If End If
' 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
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 ***
Dim lock1Acquired As Boolean = False
Try
Main.MainConnectorsLock.RunMethod("lock", Null) Main.MainConnectorsLock.RunMethod("lock", Null)
lock1Acquired = True
oldConnectors = Main.Connectors oldConnectors = Main.Connectors
Catch
sbTemp.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para leer conectores antiguos: ${LastException.Message}"$).Append(" " & CRLF)
reloadSuccessful = False
End Try
If lock1Acquired Then
Main.MainConnectorsLock.RunMethod("unlock", Null) Main.MainConnectorsLock.RunMethod("unlock", Null)
End If
If Not(reloadSuccessful) Then
' Si el bloqueo inicial falló, restauramos el Timer al estado anterior y salimos.
If oldTimerState Then Main.timerLogs.Enabled = True
sb.Append(sbTemp.ToString)
sb.Append($"¡ERROR: La recarga de configuración falló en la fase de bloqueo inicial! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
Return
End If
' 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
Dim newRDC As RDCConnector Dim newRDC As RDCConnector
newRDC.Initialize(dbKey) ' Inicializa la nueva instancia con la configuración fresca. newRDC.Initialize(dbKey)
Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0)
' <<< PASO CLAVE 2: LEER Y ALMACENAR EL NUEVO ESTADO DE LOGS PARA CADA DB >>>
' Leemos la configuración 'enableSQLiteLogs' de esta DBkey.
Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1) Dim isEnabled As Boolean = (enableLogsSetting = 1)
' Almacenamos el estado temporalmente en el mapa newConnectors bajo una clave única.
newConnectors.Put(dbKey & "_LOG_STATE", isEnabled) newConnectors.Put(dbKey & "_LOG_STATE", isEnabled)
sbTemp.Append($" -> Logs de ${dbKey} activados: ${isEnabled}"$).Append(" " & CRLF) sbTemp.Append($" -> Logs de ${dbKey} activados: ${isEnabled}"$).Append(" " & CRLF)
' <<< FIN PASO CLAVE 2 >>>
newConnectors.Put(dbKey, newRDC) newConnectors.Put(dbKey, newRDC)
Dim newPoolStats As Map = newRDC.GetPoolStats Dim newPoolStats As Map = newRDC.GetPoolStats
sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append(" " & CRLF) sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append(" " & CRLF)
Catch Catch
sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF) sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF)
reloadSuccessful = False reloadSuccessful = False
Exit ' Si uno falla, abortamos la recarga. Exit
End Try End Try
Next Next
sb.Append(sbTemp.ToString)
If reloadSuccessful Then If reloadSuccessful Then
' 3. Si todo fue exitoso, hacemos el Hot-Swap atómico.
' *** INICIO DEL BLOQUE CRÍTICO 2: Reemplazar Main.Connectors con ReentrantLock ***
Dim lock2Acquired As Boolean = False
Try
Main.MainConnectorsLock.RunMethod("lock", Null) Main.MainConnectorsLock.RunMethod("lock", Null)
lock2Acquired = True Main.Connectors = newConnectors
Main.Connectors = newConnectors ' Reemplazamos el mapa de conectores completo por el nuevo.
Catch
sb.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para reemplazar conectores: ${LastException.Message}"$).Append(" " & CRLF)
reloadSuccessful = False
End Try
If lock2Acquired Then
Main.MainConnectorsLock.RunMethod("unlock", Null) Main.MainConnectorsLock.RunMethod("unlock", Null)
End If Main.SQLiteLoggingStatusByDB.Clear
If reloadSuccessful Then ' Si el swap fue exitoso
' <<< PASO CLAVE 3: APLICAR EL NUEVO ESTADO GLOBAL GRANULAR Y REINICIAR TIMER >>>
' 3a. Reemplazar el mapa de estados de logging granular
Main.SQLiteLoggingStatusByDB.Clear ' Limpiamos el mapa global
Dim isAnyEnabled As Boolean = False Dim isAnyEnabled As Boolean = False
For Each dbKey As String In Main.listaDeCP For Each dbKey As String In Main.listaDeCP
' Recuperamos el estado logueado temporalmente. Dim isEnabled As Boolean = newConnectors.Get(dbKey & "_LOG_STATE")
Dim isEnabled As Boolean = newConnectors.Get(dbKey & "_LOG_STATE").As(Boolean) Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled)
Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled) ' Aplicamos el estado al mapa global If isEnabled Then isAnyEnabled = True
If isEnabled Then isAnyEnabled = True ' Calculamos el flag general
Next Next
Main.IsAnySQLiteLoggingEnabled = isAnyEnabled
' 3b. Controlar el Timer y el flag global
Main.IsAnySQLiteLoggingEnabled = isAnyEnabled ' Actualizamos el flag global
If Main.IsAnySQLiteLoggingEnabled Then If Main.IsAnySQLiteLoggingEnabled Then
Main.timerLogs.Enabled = True Main.timerLogs.Enabled = True
sb.Append($" -> Logs de SQLite HABILITADOS (Granular). Timer de limpieza ACTIVADO."$).Append(" " & CRLF) sbTemp.Append($" -> Logs de SQLite HABILITADOS (Granular). Timer de limpieza ACTIVADO."$).Append(" " & CRLF)
Else Else
Main.timerLogs.Enabled = False Main.timerLogs.Enabled = False
sb.Append($" -> Logs de SQLite DESHABILITADOS (Total). Timer de limpieza PERMANECERÁ DETENIDO."$).Append(" " & CRLF) sbTemp.Append($" -> Logs de SQLite DESHABILITADOS (Total). Timer de limpieza PERMANECERÁ DETENIDO."$).Append(" " & CRLF)
End If End If
' <<< FIN PASO CLAVE 3 >>> sbTemp.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append(" " & CRLF)
sb.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append(" " & CRLF)
' ... (Resto del código: Mostrar estado de pools y Cierre explícito de oldConnectors) ...
Else Else
sb.Append($"¡ERROR: La recarga de configuración falló en la fase de reemplazo de conectores! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
End If
Else ' Falla en inicialización (Punto 2)
' Si falla la recarga, restauramos el Timer al estado anterior.
If oldTimerState Then If oldTimerState Then
Main.timerLogs.Enabled = True Main.timerLogs.Enabled = True
sb.Append(" -> Restaurando Timer de limpieza de logs (SQLite) al estado ACTIVO debido a fallo en recarga.").Append(" " & CRLF) sbTemp.Append(" -> Restaurando Timer de limpieza de logs (SQLite) al estado ACTIVO debido a fallo en recarga.").Append(" " & CRLF)
End If
sbTemp.Append($"¡ERROR: La recarga de configuración falló! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
End If End If
sb.Append($"¡ERROR: La recarga de configuración falló durante la inicialización de nuevos conectores! Los conectores antiguos siguen activos."$).Append(" " & CRLF) ' <<< CAMBIO: Se devuelve el contenido del StringBuilder como texto plano >>>
End If resp.Write(sbTemp.ToString)
Return
Else If Command = "slowqueries" Then ' <<< INICIO: NUEVA Lógica para mostrar las queries lentas Case "test"
sb.Append("<h2 style=""margin-top:0px;margin-bottom:0px;"">Consultas Lentas Recientes</h2>") resp.ContentType = "text/plain; charset=utf-8"
sb.Append("(Este registro depende de que los logs estén habilitados con del parámetro ""enableSQLiteLogs=1"" en config properties)<br><br>") Dim sb As StringBuilder
Try sb.Initialize
' 1. Calcular el límite de tiempo: el tiempo actual (en milisegundos) menos 1 hora (3,600,000 ms).
Dim oneHourAgoMs As Long = DateTime.Now - 3600000
' Ajusta la consulta SQL para obtener las 20 queries más lentas.
' Utilizamos datetime con 'unixepoch' y 'localtime' para una visualización legible del timestamp.
Dim rs As ResultSet = Main.SQL1.ExecQuery($"SELECT query_name, duration_ms, datetime(timestamp / 1000, 'unixepoch', 'localtime') as timestamp_local, db_key, client_ip, busy_connections, handler_active_requests FROM query_logs WHERE timestamp >= ${oneHourAgoMs} ORDER BY duration_ms DESC LIMIT 20"$)
sb.Append("<table border='1' style='width:100%; text-align:left; border-collapse: collapse;'>")
sb.Append("<thead><tr><th>Query</th><th>Duración (ms)</th><th>Fecha/Hora Local</th><th>DB Key</th><th>Cliente IP</th><th>Conex. Ocupadas</th><th>Peticiones Activas</th></tr></thead>")
sb.Append("<tbody>")
Do While rs.NextRow
sb.Append("<tr>")
sb.Append($"<td>${rs.GetString("query_name")}</td>"$)
sb.Append($"<td>${rs.GetLong("duration_ms")}</td>"$)
sb.Append($"<td>${rs.GetString("timestamp_local")}</td>"$)
sb.Append($"<td>${rs.GetString("db_key")}</td>"$)
sb.Append($"<td>${rs.GetString("client_ip")}</td>"$)
sb.Append($"<td>${rs.GetInt("busy_connections")}</td>"$)
sb.Append($"<td>${rs.GetInt("handler_active_requests")}</td>"$)
sb.Append("</tr>")
Loop
sb.Append("</tbody>")
sb.Append("</table>")
rs.Close
Catch
Log("Error al obtener queries lentas en Manager: " & LastException.Message)
sb.Append($"<p style='color:red;'>Error al cargar queries lentas: ${LastException.Message}</p>"$)
End Try
Else If Command = "test" Then
Try Try
Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("") Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("")
sb.Append("Connection successful.</br></br>") sb.Append("Connection successful." & CRLF & CRLF)
Private estaDB As String = "" Dim estaDB As String = ""
Log(Main.listaDeCP) Log(Main.listaDeCP)
For i = 0 To Main.listaDeCP.Size - 1 For i = 0 To Main.listaDeCP.Size - 1
If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i) If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i)
sb.Append($"Using config${estaDB}.properties<br/>"$) sb.Append($"Using config${estaDB}.properties"$ & CRLF)
Next Next
con.Close con.Close
resp.Write(sb.ToString)
Catch Catch
resp.Write("Error fetching connection.") resp.Write("Error fetching connection: " & LastException.Message)
End Try End Try
Else If Command = "stop" Then Return
' Public shl As Shell...
Else If Command = "rsx" Then Case "rsx", "rpm2", "revivebow", "restartserver"
Log($"Ejecutamos ${File.DirApp}\start.bat"$) resp.ContentType = "text/plain; charset=utf-8"
sb.Append($"Ejecutamos ${File.DirApp}\start.bat"$) Dim batFile As String
Public shl As Shell Select Command
shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\start.bat " & Main.srvr.Port)) Case "rsx": batFile = "start.bat"
Case "rpm2": batFile = "reiniciaProcesoPM2.bat"
Case "reviveBow": batFile = "reiniciaProcesoBow.bat"
Case "restartserver": batFile = "restarServer.bat"
End Select
Log($"Ejecutando ${File.DirApp}\${batFile}"$)
Try
Dim shl As Shell
shl.Initialize("shl","cmd", Array("/c", File.DirApp & "\" & batFile & " " & Main.srvr.Port))
shl.WorkingDirectory = File.DirApp shl.WorkingDirectory = File.DirApp
shl.Run(-1) shl.Run(-1)
Else If Command = "rpm2" Then resp.Write($"Comando '${Command}' ejecutado. Script invocado: ${batFile}"$)
Catch
resp.Write($"Error al ejecutar el script para '${Command}': ${LastException.Message}"$)
End Try
Return
Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$)
sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$)
Public shl As Shell Public shl As Shell
shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoPM2.bat " & Main.srvr.Port)) shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoPM2.bat " & Main.srvr.Port))
shl.WorkingDirectory = File.DirApp shl.WorkingDirectory = File.DirApp
shl.Run(-1) shl.Run(-1)
Else If Command = "reviveBow" Then
Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoBow.bat"$) Case "paused", "continue"
sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoBow.bat<br><br>"$) resp.ContentType = "text/plain; charset=utf-8"
sb.Append($"!!!BOW REINICIANDO!!!"$) If Command = "paused" Then
Public shl As Shell
shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoBow.bat " & Main.srvr.Port))
shl.WorkingDirectory = File.DirApp
shl.Run(-1)
Else If Command = "paused" Then
GlobalParameters.IsPaused = 1 GlobalParameters.IsPaused = 1
sb.Append("Servidor pausado.") resp.Write("Servidor pausado.")
Else If Command = "continue" Then
GlobalParameters.IsPaused = 0
sb.Append("Servidor reanudado.")
Else If Command = "logs" Then
If GlobalParameters.mpLogs.IsInitialized Then
j.Initialize(GlobalParameters.mpLogs)
sb.Append(j.ToString)
End If
Else If Command = "block" Then
Dim BlockedConIP As String = req.GetParameter("IP")
If GlobalParameters.mpBlockConnection.IsInitialized Then
GlobalParameters.mpBlockConnection.Put(BlockedConIP, BlockedConIP)
sb.Append("IP bloqueada: " & BlockedConIP)
End If
Else If Command = "unblock" Then
Dim UnBlockedConIP As String = req.GetParameter("IP")
If GlobalParameters.mpBlockConnection.IsInitialized Then
GlobalParameters.mpBlockConnection.Remove(UnBlockedConIP)
sb.Append("IP desbloqueada: " & UnBlockedConIP)
End If
Else If Command = "restartserver" Then
Log($"Ejecutamos ${File.DirApp}/restarServer.bat"$)
sb.Append("Reiniciando servidor...")
Else If Command = "runatstartup" Then
File.Copy("C:\jrdcinterface", "startup.bat", "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp", "startup.bat")
sb.Append("Script de inicio añadido.")
Else If Command = "stoprunatstartup" Then
File.Delete("C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp", "startup.bat")
sb.Append("Script de inicio eliminado.")
Else If Command = "totalrequests" Then
If GlobalParameters.mpTotalRequests.IsInitialized Then
j.Initialize(GlobalParameters.mpTotalRequests)
sb.Append(j.ToString)
End If
Else If Command = "totalblocked" Then
If GlobalParameters.mpBlockConnection.IsInitialized Then
' j.Initialize(Global.mpBlockConnection)
sb.Append(j.ToString)
End If
Else If Command = "ping" Then
sb.Append($"Pong ($DateTime{DateTime.Now})"$)
Else If Command = "totalcon" Then ' <<< Modificado: Ahora usa GetPoolStats para cada pool
' Verificamos que el mapa global de conexiones esté inicializado.
' Aunque no lo poblamos directamente, es un buen chequeo de estado.
If GlobalParameters.mpTotalConnections.IsInitialized Then
sb.Append("<h2>Estadísticas del Pool de Conexiones por DB:</h2>")
' Creamos un mapa LOCAL para almacenar las estadísticas de TODOS los pools de conexiones.
Dim allPoolStats As Map
allPoolStats.Initialize
' Iteramos sobre cada clave de base de datos que tenemos configurada (DB1, DB2, etc.).
For Each dbKey As String In Main.listaDeCP
' Obtenemos el conector RDC para la base de datos actual.
Dim connector As RDCConnector = Main.Connectors.Get(dbKey).As(RDCConnector)
' Si el conector no está inicializado (lo cual no debería ocurrir si Main.AppStart funcionó),
' registramos un error y pasamos al siguiente.
If connector.IsInitialized = False Then
Log($"Manager: ADVERTENCIA: El conector para ${dbKey} no está inicializado."$)
Dim errorMap As Map = CreateMap("Error": "Conector no inicializado o no cargado correctamente")
allPoolStats.Put(dbKey, errorMap)
Continue ' Salta a la siguiente iteración del bucle.
End If
' Llamamos al método GetPoolStats del conector para obtener las métricas de su pool.
Dim poolStats As Map = connector.GetPoolStats
' Añadimos las estadísticas de este pool (poolStats) al mapa general (allPoolStats),
' usando la clave de la base de datos (dbKey) como su identificador.
allPoolStats.Put(dbKey, poolStats)
Next
' Inicializamos el generador JSON con el mapa 'allPoolStats' (que ahora sí debería contener datos).
' (La variable 'j' ya está declarada en Class_Globals de Manager.bas, no la declares de nuevo aquí).
j.Initialize(allPoolStats)
' Añadimos la representación JSON de las estadísticas al StringBuilder para la respuesta HTML.
sb.Append(j.ToString)
Else Else
sb.Append("El mapa de conexiones GlobalParameters.mpTotalConnections no está inicializado.") GlobalParameters.IsPaused = 0
resp.Write("Servidor reanudado.")
End If End If
Return
Case "block", "unblock"
resp.ContentType = "text/plain; charset=utf-8"
Dim ip As String = req.GetParameter("IP")
If ip = "" Then
resp.Write("Error: El parámetro IP es requerido.")
Return
End If End If
' ========================================================================= If GlobalParameters.mpBlockConnection.IsInitialized Then
' ### FIN DE TU LÓGICA DE COMANDOS ### If Command = "block" Then
' ========================================================================= GlobalParameters.mpBlockConnection.Put(ip, ip)
resp.Write($"IP bloqueada: ${ip}"$)
Else
GlobalParameters.mpBlockConnection.Remove(ip)
resp.Write($"IP desbloqueada: ${ip}"$)
End If
Else
resp.Write("Error: El mapa de bloqueo no está inicializado.")
End If
Return
' --- Cerramos la página y la enviamos --- Case Else
sb.Append("</div><p class='logout'><a href='/logout'>Cerrar Sesión</a> | <a href=# onclick='toggleForm()'>Cambiar Contraseña</a></p></body></html>") resp.ContentType = "text/plain; charset=utf-8"
resp.Write(sb.ToString) resp.SendError(404, $"Comando desconocido: '{Command}'"$)
Return
If GlobalParameters.mpLogs.IsInitialized Then GlobalParameters.mpLogs.Put(Command, "Manager : " & Command & " - Time : " & DateTime.Time(DateTime.Now)) End Select
End Sub End Sub

View File

@@ -50,6 +50,7 @@ Public Sub ValidateAndAdjustParameters (CommandName As String, DBKey As String,
res.Success = True res.Success = True
Dim WarningMsg As String = $"ADVERTENCIA: Se recibieron más parámetros de los esperados para "${CommandName}" (DB: ${DBKey}). Se esperaban ${expectedParams} y se recibieron ${receivedParamsSize}. Se ajustó la lista de parámetros a ${expectedParams} elementos."$ Dim WarningMsg As String = $"ADVERTENCIA: Se recibieron más parámetros de los esperados para "${CommandName}" (DB: ${DBKey}). Se esperaban ${expectedParams} y se recibieron ${receivedParamsSize}. Se ajustó la lista de parámetros a ${expectedParams} elementos."$
Log(WarningMsg) Log(WarningMsg)
Log("Cache: " & Main.LOG_CACHE_THRESHOLD & "|" & Main.ErrorLogCache.Size)
Main.LogServerError("ADVERTENCIA", "ParameterValidationUtils.ValidateAndAdjustParameters", WarningMsg, DBKey, CommandName, Null) ' <-- Nuevo Log [6] Main.LogServerError("ADVERTENCIA", "ParameterValidationUtils.ValidateAndAdjustParameters", WarningMsg, DBKey, CommandName, Null) ' <-- Nuevo Log [6]
Else Else
' Si la tolerancia NO está habilitada, esto es un error crítico. ' Si la tolerancia NO está habilitada, esto es un error crítico.

View File

@@ -32,10 +32,11 @@ Library8=jsql
Library9=bcrypt Library9=bcrypt
Module1=Cambios Module1=Cambios
Module10=Manager Module10=Manager
Module11=ParameterValidationUtils Module11=Manager0
Module12=ping Module12=ParameterValidationUtils
Module13=RDCConnector Module13=ping
Module14=TestHandler Module14=RDCConnector
Module15=TestHandler
Module2=ChangePassHandler Module2=ChangePassHandler
Module3=DBHandlerB4X Module3=DBHandlerB4X
Module4=DBHandlerJSON Module4=DBHandlerJSON
@@ -46,7 +47,7 @@ Module8=LoginHandler
Module9=LogoutHandler Module9=LogoutHandler
NumberOfFiles=10 NumberOfFiles=10
NumberOfLibraries=9 NumberOfLibraries=9
NumberOfModules=14 NumberOfModules=15
Version=10.3 Version=10.3
@EndOfDesignText@ @EndOfDesignText@
'Non-UI application (console / server application) 'Non-UI application (console / server application)
@@ -55,7 +56,7 @@ Version=10.3
#CommandLineArgs: #CommandLineArgs:
#MergeLibraries: True #MergeLibraries: True
' VERSION 5.09.16 ' VERSION 5.09.17
'########################################################################################################### '###########################################################################################################
'###################### 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
@@ -112,6 +113,7 @@ Sub Process_Globals
' NUEVAS VARIABLES para control granular de logs ' NUEVAS VARIABLES para control granular de logs
' Mapa para almacenar el estado de logging (True/False) por cada DBKey (DB1, DB2, etc.). ' Mapa para almacenar el estado de logging (True/False) por cada DBKey (DB1, DB2, etc.).
Public SQLiteLoggingStatusByDB As Map Public SQLiteLoggingStatusByDB As Map
' Bandera global que indica si AL MENOS una base de datos tiene los logs habilitados. ' Bandera global que indica si AL MENOS una base de datos tiene los logs habilitados.
Public IsAnySQLiteLoggingEnabled As Boolean Public IsAnySQLiteLoggingEnabled As Boolean
@@ -121,12 +123,26 @@ Sub Process_Globals
ErrorMessage As String, _ ErrorMessage As String, _
ParamsToExecute As List _ ' La lista de parámetros final a usar en la ejecución SQL ParamsToExecute As List _ ' La lista de parámetros final a usar en la ejecución SQL
) )
Public QueryLogCache As List ' Cache para los logs de rendimiento (query_logs)
Public ErrorLogCache As List ' Cache para los logs de errores y advertencias
Public LOG_CACHE_THRESHOLD As Int = 350 ' Umbral de registros para forzar la escritura
Dim logger As Boolean
End Sub End Sub
Sub AppStart (Args() As String) Sub AppStart (Args() As String)
#if DEBUG
logger = True
LOG_CACHE_THRESHOLD = 10
#else
logger = False
#End If
' --- Subrutina principal que se ejecuta al iniciar la aplicación --- ' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
bc.Initialize("BC") bc.Initialize("BC")
QueryLogCache.Initialize
ErrorLogCache.Initialize
' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs. ' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs.
InitializeSQLiteDatabase InitializeSQLiteDatabase
@@ -164,7 +180,7 @@ Sub AppStart (Args() As String)
Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$) Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$)
' Lógica de Logs para DB1 (Fuente principal de configuración) ' Lógica de Logs para DB1 (Fuente principal de configuración)
Dim enableLogsSetting As Int = con1.config.GetDefault("enableSQLiteLogs", 1).As(Int) Dim enableLogsSetting As Int = con1.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1) Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put("DB1", isEnabled) ' Guardar el estado SQLiteLoggingStatusByDB.Put("DB1", isEnabled) ' Guardar el estado
@@ -276,10 +292,10 @@ Sub AppStart (Args() As String)
If IsAnySQLiteLoggingEnabled Then If IsAnySQLiteLoggingEnabled Then
timerLogs.Enabled = True timerLogs.Enabled = True
Log("Main.AppStart: Timer de limpieza de logs ACTIVADO (al menos una DB requiere logs).") If logger Then Log("Main.AppStart: Timer de limpieza de logs ACTIVADO (al menos una DB requiere logs).")
Else Else
timerLogs.Enabled = False timerLogs.Enabled = False
Log("Main.AppStart: Timer de limpieza de logs DESHABILITADO (ninguna DB requiere logs).") If logger Then Log("Main.AppStart: Timer de limpieza de logs DESHABILITADO (ninguna DB requiere logs).")
End If End If
' <<<< Fin del bloque del Timer >>>> ' <<<< Fin del bloque del Timer >>>>
@@ -321,7 +337,7 @@ Sub InitializeSQLiteDatabase
SQL1.ExecNonQuery(createUserTable) SQL1.ExecNonQuery(createUserTable)
' Crear tabla 'query_logs' ' Crear tabla 'query_logs'
Log("Creando tabla 'query_logs' con columnas de rendimiento.") If logger Then Log("Creando tabla 'query_logs' 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)
@@ -342,11 +358,11 @@ Sub InitializeSQLiteDatabase
Log("Base de datos de usuarios cargada.") Log("Base de datos de usuarios cargada.")
' >>> 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.") If logger Then Log("Verificando y migrando tabla 'query_logs' si es necesario.")
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.") If logger Then Log("Tabla 'query_logs' no encontrada, creándola con columnas de rendimiento.")
Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)" 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)"
@@ -371,7 +387,7 @@ Sub InitializeSQLiteDatabase
rs.Close rs.Close
If columnExists = False Then If columnExists = False Then
Log("Añadiendo columna 'busy_connections' a query_logs.") If logger Then Log("Añadiendo columna 'busy_connections' a query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0") SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0")
End If End If
@@ -388,20 +404,34 @@ Sub InitializeSQLiteDatabase
rs.Close rs.Close
If columnExists = False Then If columnExists = False Then
Log("Añadiendo columna 'handler_active_requests' a query_logs.") If logger Then Log("Añadiendo columna 'handler_active_requests' a query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0") SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0")
End If End If
columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("timestamp_text_local") Then
columnExists = True
Exit ' La columna ya existe, salimos del bucle.
End If
Loop
rs.Close
If columnExists = False Then
If logger Then Log("Añadiendo columna 'timestamp_text_local' a query_logs.")
' Usamos 'TEXT' para almacenar la cadena de fecha/hora formateada.
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN timestamp_text_local TEXT")
End If
' >>> INICIO: Lógica de migración para 'errores' si la DB ya existía <<< ' >>> INICIO: Lógica de migración para 'errores' si la DB ya existía <<<
Log("Verificando y migrando tabla 'errores' si es necesario.") If logger Then Log("Verificando y migrando tabla 'errores' si es necesario.")
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then
Log("Tabla 'errores' no encontrada, creándola.") If logger Then Log("Tabla 'errores' no encontrada, creándola.")
Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)" Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)"
SQL1.ExecNonQuery(createErrorsTable) SQL1.ExecNonQuery(createErrorsTable)
Else Else
Log("Tabla 'errores' ya existe.") If logger Then Log("Tabla 'errores' ya existe.")
End If End If
' >>> FIN: Lógica de migración para 'errores' <<< ' >>> FIN: Lógica de migración para 'errores' <<<
@@ -411,45 +441,155 @@ Sub InitializeSQLiteDatabase
End If End If
End Sub End Sub
' --- Subrutina para registrar las métricas de rendimiento de las queries en la tabla 'query_logs'. ---
' ¡MODIFICADA PARA USAR FILTRADO GRANULAR POR DBKEY!
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)
' Obtener el estado de logging para esta DBKey. Usar False si la DBKey no existe en el mapa.
Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DbKey, False) Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DbKey, False)
If isEnabled Then If isEnabled Then
Try
SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests) VALUES (?, ?, ?, ?, ?, ?, ?)", _ ' Formato de tiempo necesario para la columna timestamp_text_local
Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests)) DateTime.DateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
Catch Dim formattedTimestamp As String = DateTime.Date(DateTime.Now)
Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message)
End Try ' 1. Crear el mapa de datos (log entry)
Dim logEntry As Map = CreateMap("query_name": QueryName, "duration_ms": DurationMs, "timestamp": DateTime.Now, _
"db_key": DbKey, "client_ip": ClientIp, "busy_connections": PoolBusyConnections, _
"handler_active_requests": HandlerActiveRequests, "timestamp_text_local": formattedTimestamp)
' 2. Zona Crítica: Añadir a la caché y verificar el umbral
Dim shouldWriteBatch As Boolean = False
' Usamos el lock global para garantizar que la adición y la verificación del tamaño sean atómicas.
MainConnectorsLock.RunMethod("lock", Null)
QueryLogCache.Add(logEntry)
If QueryLogCache.Size >= LOG_CACHE_THRESHOLD Then
shouldWriteBatch = True
End If
MainConnectorsLock.RunMethod("unlock", Null)
' 3. Si se alcanzó el umbral, disparamos la escritura.
' NO DEBE HACERSE CON EL LOCK PUESTO.
If shouldWriteBatch Then
CallSub(Me, "WriteQueryLogsBatch")
End If
End If End If
End Sub End Sub
' --- Subrutina para registrar errores y advertencias en la tabla 'errores'. --- ' --- Subrutina para registrar errores y advertencias en la tabla 'errores'. ---
' ¡MODIFICADA PARA USAR FILTRADO GRANULAR POR DBKEY!
Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String) Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String)
' Obtener el estado de logging para esta DBKey. Usar False si la DBKey es Null o no está en el mapa.
Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DBKey, False) Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DBKey, False)
If isEnabled Then If isEnabled Then
Try
SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _ ' Log($"[DEBUG CACHE] Se recibió log de error/advertencia para: ${CommandName}"$) '<--- Nuevo Log 1
Array As Object(DateTime.Now, Type0, Source, Message, DBKey, CommandName, ClientIp))
Catch Dim logEntry As Map = CreateMap("timestamp": DateTime.Now, "type": Type0, "source": Source, "message": Message, _
Log("ERROR CRÍTICO: Fallo al guardar el log de error/advertencia en SQLite (Main.LogServerError): " & LastException.Message) "db_key": DBKey, "command_name": CommandName, "client_ip": ClientIp)
End Try
Dim shouldWriteBatch As Boolean = False
' 1. Zona Crítica: Añadir a la caché y verificar el umbral
' Usamos el lock para Thread Safety
MainConnectorsLock.RunMethod("lock", Null)
' Log($"[DEBUG CACHE] Lock adquirido. Tamaño actual de ErrorLogCache: ${ErrorLogCache.Size}"$) '<--- Nuevo Log 2
ErrorLogCache.Add(logEntry)
' Log($"[DEBUG CACHE] Log añadido. Nuevo tamaño: ${ErrorLogCache.Size}. Umbral: ${LOG_CACHE_THRESHOLD}"$) '<--- Nuevo Log 3
If ErrorLogCache.Size >= LOG_CACHE_THRESHOLD Then
shouldWriteBatch = True
' Log(">>> [DEBUG CACHE] UMBRAL ALCANZADO. DISPARANDO ESCRITURA BATCH. <<<") '<--- Nuevo Log 4
End If End If
MainConnectorsLock.RunMethod("unlock", Null)
' Log($"[DEBUG CACHE] Lock liberado."$) '<--- Nuevo Log 5
' 2. Si se alcanzó el umbral (o si el timer lo llama), disparamos la escritura.
If shouldWriteBatch Then
CallSub(Me, "WriteErrorLogsBatch")
End If
Else
' Log($"[DEBUG CACHE] Logging deshabilitado para DBKey: ${DBKey}. Log de error omitido."$)
End If
End Sub
Public Sub WriteQueryLogsBatch
Dim logsToWrite As List
logsToWrite.Initialize ' 1. Inicializar la lista local (CRÍTICO)
' === PASO 1: Intercambio Atómico de Caché (Protegido por ReentrantLock) ===
MainConnectorsLock.RunMethod("lock", Null)
If QueryLogCache.Size = 0 Then
MainConnectorsLock.RunMethod("unlock", Null)
' Log("[DEBUG BATCH-Q] Saliendo: Caché de rendimiento vacía.")
Return
End If
' *** CORRECCIÓN CRÍTICA: Copia de contenido (AddAll) en lugar de referencia. ***
logsToWrite.AddAll(QueryLogCache)
Dim batchSize As Int = logsToWrite.Size
' Vaciamos la caché global. logsToWrite ahora contiene la copia de los elementos.
QueryLogCache.Initialize
MainConnectorsLock.RunMethod("unlock", Null)
If logger Then Log($"[LOG BATCH] Iniciando escritura transaccional de ${batchSize} logs de rendimiento. Logs copiados: ${logsToWrite.Size}"$)
' === PASO 2: Escritura Transaccional a SQLite ===
Try
' 1. Iniciar la transacción: Todo lo que siga es una única operación de disco.
SQL1.BeginTransaction
For Each logEntry As Map In logsToWrite
SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests, timestamp_text_local) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", _
Array As Object(logEntry.Get("query_name"), logEntry.Get("duration_ms"), logEntry.Get("timestamp"), logEntry.Get("db_key"), _
logEntry.Get("client_ip"), logEntry.Get("busy_connections"), logEntry.Get("handler_active_requests"), _
logEntry.Get("timestamp_text_local")))
Next
' 2. Finalizar la transacción: Escritura eficiente a disco.
SQL1.TransactionSuccessful
if logger then Log($"[LOG BATCH] Lote de ${batchSize} logs de rendimiento escrito exitosamente."$)
Catch
' Si falla, deshacemos todos los logs del lote y registramos el fallo.
SQL1.Rollback
Dim ErrorMsg As String = "ERROR CRÍTICO: Fallo al escribir lote de logs de rendimiento en SQLite: " & LastException.Message
Log(ErrorMsg)
' Usamos LogServerError para que el fallo quede registrado en la tabla 'errores' si el logging está habilitado.
LogServerError("ERROR", "Main.WriteQueryLogsBatch", ErrorMsg, Null, "log_batch_write_performance", Null)
End Try
End Sub End Sub
' --- Subrutina de evento para el Timer 'timerLogs'. --- ' --- Subrutina de evento para el Timer 'timerLogs'. ---
' El estado 'Enabled' del Timer ya está controlado por IsAnySQLiteLoggingEnabled en AppStart y Manager. ' El estado 'Enabled' del Timer ya está controlado por IsAnySQLiteLoggingEnabled en AppStart y Manager.
Sub TimerLogs_Tick Sub TimerLogs_Tick
Try Try
' 1. Vaciado de logs de rendimiento (asumiendo que WriteQueryLogsBatch también fue implementado)
WriteQueryLogsBatch
' 2. Vaciado de logs de errores
WriteErrorLogsBatch
' 3. Limpieza y VACUUM (esto ya verifica IsAnySQLiteLoggingEnabled [8])
borraArribaDe15000Logs borraArribaDe15000Logs
Catch Catch
Dim ErrorMsg As String = "ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message Dim ErrorMsg As String = "ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message
Log(ErrorMsg) Log(ErrorMsg)
@@ -457,16 +597,85 @@ Sub TimerLogs_Tick
End Try End Try
End Sub End Sub
Public Sub WriteErrorLogsBatch
Dim logsToWrite As List
logsToWrite.Initialize ' *** Aseguramos que logsToWrite sea una LISTA NUEVA y no dependa de la referencia.
' === PASO 1: Intercambio Atómico de Caché (Protegido por ReentrantLock) ===
MainConnectorsLock.RunMethod("lock", Null) ' Adquirimos el bloqueo.
' Log($"[DEBUG BATCH] Lock adquirido en WriteErrorLogsBatch. Caché Size: ${ErrorLogCache.Size}"$)
If ErrorLogCache.Size = 0 Then
MainConnectorsLock.RunMethod("unlock", Null)
' Log("[DEBUG BATCH] Saliendo: Caché vacía.")
Return
End If
' *** CORRECCIÓN CRÍTICA: Copiamos el CONTENIDO de forma atómica. ***
logsToWrite.AddAll(ErrorLogCache) ' <--- ESTO PASA LOS 10 REGISTROS A LA NUEVA LISTA
' Vaciamos la caché global. logsToWrite AHORA ES INDEPENDIENTE.
ErrorLogCache.Initialize
MainConnectorsLock.RunMethod("unlock", Null) ' Liberamos el bloqueo.
' Usamos el tamaño de la lista *copiada*.
Dim batchSize As Int = logsToWrite.Size
If logger Then Log($"[LOG BATCH] Iniciando escritura transaccional de ${batchSize} logs de ERRORES a SQLite. Logs copiados: ${logsToWrite.Size}"$)
' === PASO 2: Escritura Transaccional a SQLite (Usa logsToWrite) ===
If batchSize = 0 Then
Log("ADVERTENCIA: Fallo en la copia de la lista. logsToWrite está vacía. Abortando escritura.")
Return
End If
Try
' 1. Iniciar la transacción.
SQL1.BeginTransaction
For Each logEntry As Map In logsToWrite
' ... (Tu lógica de SQL1.ExecNonQuery2 aquí) ...
SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _
Array As Object(logEntry.Get("timestamp"), logEntry.Get("type"), logEntry.Get("source"), logEntry.Get("message"), _
logEntry.Get("db_key"), logEntry.Get("command_name"), logEntry.Get("client_ip")))
Next
' 2. Confirmar la transacción.
SQL1.TransactionSuccessful
If logger Then Log($"[LOG BATCH] Lote de ${logsToWrite.Size} logs de ERRORES escrito exitosamente."$)
Catch
' 3. Rollback si falla.
SQL1.Rollback
Dim ErrorMsg As String = "ERROR CRÍTICO: Fallo al escribir lote de logs de ERRORES en SQLite: " & LastException.Message
Log(ErrorMsg)
End Try
End Sub
' --- Borra los registros más antiguos de la tabla 'query_logs' y hace VACUUM. --- ' --- Borra los registros más antiguos de la tabla 'query_logs' y hace VACUUM. ---
' ¡MODIFICADA PARA USAR FILTRADO GLOBAL! ' ¡MODIFICADA PARA USAR FILTRADO GLOBAL!
Sub borraArribaDe15000Logs 'ignore Sub borraArribaDe15000Logs 'ignore
If IsAnySQLiteLoggingEnabled Then ' Solo ejecutar si al menos una DB requiere logs. If IsAnySQLiteLoggingEnabled Then ' Solo ejecutar si al menos una DB requiere logs.
Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.") If logger Then Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.")
' 1. Limpieza de Logs de Rendimiento (query_logs)
If logger Then 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("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )")
' 2. Limpieza de Logs de Errores (errores)
If logger Then Log("Recortando la tabla de 'errores', límite de 15,000 registros.")
SQL1.ExecNonQuery("DELETE FROM errores WHERE timestamp NOT in (SELECT timestamp FROM errores ORDER BY timestamp desc LIMIT 15000 )")
' 3. Optimización de disco
SQL1.ExecNonQuery("vacuum;") SQL1.ExecNonQuery("vacuum;")
Else Else
' Si IsAnySQLiteLoggingEnabled es False, el Timer no debería estar activo. ' Si IsAnySQLiteLoggingEnabled es False, el Timer no debería estar activo.
Log("AVISO: Tarea de limpieza de logs omitida. El logging global de SQLite está deshabilitado.") If logger Then Log("AVISO: Tarea de limpieza de logs omitida. El logging global de SQLite está deshabilitado.")
End If End If
End Sub End Sub

View File

@@ -5,6 +5,7 @@ ModuleBookmarks11=
ModuleBookmarks12= ModuleBookmarks12=
ModuleBookmarks13= ModuleBookmarks13=
ModuleBookmarks14= ModuleBookmarks14=
ModuleBookmarks15=
ModuleBookmarks2= ModuleBookmarks2=
ModuleBookmarks3= ModuleBookmarks3=
ModuleBookmarks4= ModuleBookmarks4=
@@ -20,6 +21,7 @@ ModuleBreakpoints11=
ModuleBreakpoints12= ModuleBreakpoints12=
ModuleBreakpoints13= ModuleBreakpoints13=
ModuleBreakpoints14= ModuleBreakpoints14=
ModuleBreakpoints15=
ModuleBreakpoints2= ModuleBreakpoints2=
ModuleBreakpoints3= ModuleBreakpoints3=
ModuleBreakpoints4= ModuleBreakpoints4=
@@ -35,14 +37,15 @@ ModuleClosedNodes11=
ModuleClosedNodes12= ModuleClosedNodes12=
ModuleClosedNodes13= ModuleClosedNodes13=
ModuleClosedNodes14= ModuleClosedNodes14=
ModuleClosedNodes15=
ModuleClosedNodes2= ModuleClosedNodes2=
ModuleClosedNodes3=9,10,11,12,13,14,15,16 ModuleClosedNodes3=
ModuleClosedNodes4= ModuleClosedNodes4=
ModuleClosedNodes5= ModuleClosedNodes5=
ModuleClosedNodes6= ModuleClosedNodes6=
ModuleClosedNodes7= ModuleClosedNodes7=
ModuleClosedNodes8= ModuleClosedNodes8=
ModuleClosedNodes9= ModuleClosedNodes9=
NavigationStack=DBHandlerB4X,CleanupAndLog,198,0,DBHandlerJSON,CleanupAndLog,223,0,ParameterValidationUtils,ValidateAndAdjustParameters,45,0,Main,Process_Globals,53,0,Main,AppStart,186,0,Main,LogQueryPerformance,367,0,Main,LogServerError,384,6,Manager,Handle,164,6,Main,borraArribaDe15000Logs,412,0,Cambios,Process_Globals,25,6 NavigationStack=DBHandlerJSON,SendSuccessResponse,253,0,DBHandlerJSON,CleanupAndLog,248,0,RDCConnector,Class_Globals,21,0,RDCConnector,Initialize,35,0,Main,LogServerError,453,0,DBHandlerB4X,ExecuteBatch2,342,0,DBHandlerJSON,Class_Globals,7,0,DBHandlerB4X,ExecuteBatch,445,6,DBHandlerJSON,Handle,201,6,Main,borraArribaDe15000Logs,623,0,Cambios,Process_Globals,22,0
SelectedBuild=0 SelectedBuild=0
VisibleModules=3,4,13,1,10,11,14,2 VisibleModules=3,4,14,1,10,12