From 884cb96f9da9997c8b8c2202efbfdfbb10072c29 Mon Sep 17 00:00:00 2001 From: Jose Alberto Guerra Ugalde Date: Fri, 19 Sep 2025 18:43:47 -0600 Subject: [PATCH] =?UTF-8?q?-=20VERSION=205.09.16.2=20-=20feat(logs):=20Imp?= =?UTF-8?q?lementaci=C3=B3n=20de=20Cacheo=20y=20Escritura=20Transaccional?= =?UTF-8?q?=20en=20Lotes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- Cambios.bas | 14 ++ DBHandlerB4X.bas | 23 +++- Files/config.DB2.properties | 23 +++- Files/config.DB3.properties | 29 +++- Files/config.DB4.properties | 29 +++- Files/config.properties | 23 +++- jRDC_Multi.b4j | 263 +++++++++++++++++++++++++++++++----- jRDC_Multi.b4j.meta | 2 +- 8 files changed, 347 insertions(+), 59 deletions(-) diff --git a/Cambios.bas b/Cambios.bas index bfb8be3..ae2f589 100644 --- a/Cambios.bas +++ b/Cambios.bas @@ -26,6 +26,20 @@ 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 ' lista, por ejemplo 15, 30, 45 y 60 minutos antes. +' - 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 ' - 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.). diff --git a/DBHandlerB4X.bas b/DBHandlerB4X.bas index 050a53e..c0d370d 100644 --- a/DBHandlerB4X.bas +++ b/DBHandlerB4X.bas @@ -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) ' Devuelve un resumen para el log. - Return $"batch (size=${commands.Size})"$ +' 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})"$ + End If End Sub ' --- Subrutinas para manejar la ejecución de queries y batches (Protocolo V1 - Compilación Condicional) --- @@ -422,6 +432,7 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se ' Lee cuántos comandos vienen en el lote. Dim numberOfStatements As Int = ReadInt(in) Dim res(numberOfStatements) As Int ' Array para resultados (aunque no se usa). + Dim singleQueryName As String = "" Try con.BeginTransaction @@ -430,6 +441,9 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se ' 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) + If numberOfStatements = 1 Then + singleQueryName = queryName 'Capturamos el nombre del query. + End If Dim sqlCommand As String = Connector.GetCommand(DB, queryName) ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> @@ -475,7 +489,12 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se SendPlainTextError(resp, 500, LastException.Message) End Try - Return $"batch (size=${numberOfStatements})"$ +' Return $"batch (size=${numberOfStatements})"$ + If numberOfStatements = 1 And singleQueryName <> "" Then + Return $"batch (size=1) - query: ${singleQueryName}"$ + Else + Return $"batch (size=${numberOfStatements})"$ + End If End Sub ' Ejecuta una consulta única usando el protocolo V1. diff --git a/Files/config.DB2.properties b/Files/config.DB2.properties index 70e6c4f..663c984 100644 --- a/Files/config.DB2.properties +++ b/Files/config.DB2.properties @@ -11,13 +11,18 @@ DriverClass=oracle.jdbc.driver.OracleDriver #GOHAN ---> server #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205: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 -InitialPoolSize=3 -MinPoolSize=2 -MaxPoolSize=15 -AcquireIncrement=2 +# Define el número de conexiones que se intentarán crear al iniciar el pool. +InitialPoolSize=1 +# Fija el número mínimo de conexiones que el pool mantendrá abiertas. +MinPoolSize=1 +# 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). @@ -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). 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 User=SALMA Password=SALMAD2016M diff --git a/Files/config.DB3.properties b/Files/config.DB3.properties index f98be91..57ea65f 100644 --- a/Files/config.DB3.properties +++ b/Files/config.DB3.properties @@ -11,13 +11,30 @@ DriverClass=oracle.jdbc.driver.OracleDriver #GOHAN ---> server #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205: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 -InitialPoolSize=3 -MinPoolSize=2 -MaxPoolSize=15 -AcquireIncrement=2 +# Define el número de conexiones que se intentarán crear al iniciar el pool. +InitialPoolSize=1 +# Fija el número mínimo de conexiones que el pool mantendrá abiertas. +MinPoolSize=1 +# 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 User=SALMA diff --git a/Files/config.DB4.properties b/Files/config.DB4.properties index f98be91..cf4a07f 100644 --- a/Files/config.DB4.properties +++ b/Files/config.DB4.properties @@ -11,13 +11,30 @@ DriverClass=oracle.jdbc.driver.OracleDriver #GOHAN ---> server #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205: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 -InitialPoolSize=3 -MinPoolSize=2 -MaxPoolSize=15 -AcquireIncrement=2 +# Define el número de conexiones que se intentarán crear al iniciar el pool. +InitialPoolSize=1 +# Fija el número mínimo de conexiones que el pool mantendrá abiertas. +MinPoolSize=1 +# 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 User=SALMA diff --git a/Files/config.properties b/Files/config.properties index e30705b..aace91a 100644 --- a/Files/config.properties +++ b/Files/config.properties @@ -11,13 +11,18 @@ DriverClass=oracle.jdbc.driver.OracleDriver #GOHAN ---> server #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205: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 -InitialPoolSize=3 -MinPoolSize=2 -MaxPoolSize=15 -AcquireIncrement=2 +# Define el número de conexiones que se intentarán crear al iniciar el pool. +InitialPoolSize=1 +# Fija el número mínimo de conexiones que el pool mantendrá abiertas. +MinPoolSize=1 +# 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). @@ -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). 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 User=GUNA Password=GUNAD2015M diff --git a/jRDC_Multi.b4j b/jRDC_Multi.b4j index 039ee73..353ea03 100644 --- a/jRDC_Multi.b4j +++ b/jRDC_Multi.b4j @@ -55,7 +55,7 @@ Version=10.3 #CommandLineArgs: #MergeLibraries: True -' VERSION 5.09.16 +' VERSION 5.09.16.2 '########################################################################################################### '###################### PULL ############################################################# 'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull @@ -112,6 +112,7 @@ Sub Process_Globals ' NUEVAS VARIABLES para control granular de logs ' Mapa para almacenar el estado de logging (True/False) por cada DBKey (DB1, DB2, etc.). Public SQLiteLoggingStatusByDB As Map + ' Bandera global que indica si AL MENOS una base de datos tiene los logs habilitados. Public IsAnySQLiteLoggingEnabled As Boolean @@ -121,12 +122,25 @@ Sub Process_Globals ErrorMessage As String, _ 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 Const LOG_CACHE_THRESHOLD As Int = 10 ' Umbral de registros para forzar la escritura + + Dim logger As Boolean End Sub Sub AppStart (Args() As String) + #if DEBUG + logger = True + #else + logger = False + #End If ' --- Subrutina principal que se ejecuta al iniciar la aplicación --- bc.Initialize("BC") + QueryLogCache.Initialize + ErrorLogCache.Initialize ' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs. InitializeSQLiteDatabase @@ -276,10 +290,10 @@ Sub AppStart (Args() As String) If IsAnySQLiteLoggingEnabled Then 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 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 ' <<<< Fin del bloque del Timer >>>> @@ -321,7 +335,7 @@ Sub InitializeSQLiteDatabase SQL1.ExecNonQuery(createUserTable) ' 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)" SQL1.ExecNonQuery(createQueryLogsTable) @@ -342,11 +356,11 @@ Sub InitializeSQLiteDatabase Log("Base de datos de usuarios cargada.") ' >>> 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 - 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)" @@ -371,7 +385,7 @@ Sub InitializeSQLiteDatabase rs.Close 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") End If @@ -388,20 +402,34 @@ Sub InitializeSQLiteDatabase rs.Close 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") 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 <<< - 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 - 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)" SQL1.ExecNonQuery(createErrorsTable) Else - Log("Tabla 'errores' ya existe.") + If logger Then Log("Tabla 'errores' ya existe.") End If ' >>> FIN: Lógica de migración para 'errores' <<< @@ -411,45 +439,155 @@ Sub InitializeSQLiteDatabase End If 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) - ' 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) - + 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 (?, ?, ?, ?, ?, ?, ?)", _ - Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests)) - Catch - Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message) - End Try + + ' Formato de tiempo necesario para la columna timestamp_text_local + DateTime.DateFormat = "yyyy-MM-dd HH:mm:ss.SSS" + Dim formattedTimestamp As String = DateTime.Date(DateTime.Now) + + ' 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 Sub ' --- 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) - ' 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) - + If isEnabled Then - Try - SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _ - Array As Object(DateTime.Now, Type0, Source, Message, DBKey, CommandName, ClientIp)) - Catch - Log("ERROR CRÍTICO: Fallo al guardar el log de error/advertencia en SQLite (Main.LogServerError): " & LastException.Message) - End Try + +' Log($"[DEBUG CACHE] Se recibió log de error/advertencia para: ${CommandName}"$) '<--- Nuevo Log 1 + + Dim logEntry As Map = CreateMap("timestamp": DateTime.Now, "type": Type0, "source": Source, "message": Message, _ + "db_key": DBKey, "command_name": CommandName, "client_ip": ClientIp) + + 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 + + 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 + ' --- Subrutina de evento para el Timer 'timerLogs'. --- ' El estado 'Enabled' del Timer ya está controlado por IsAnySQLiteLoggingEnabled en AppStart y Manager. Sub TimerLogs_Tick 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 + Catch Dim ErrorMsg As String = "ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message Log(ErrorMsg) @@ -457,16 +595,77 @@ Sub TimerLogs_Tick End Try 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. --- ' ¡MODIFICADA PARA USAR FILTRADO GLOBAL! Sub borraArribaDe15000Logs 'ignore 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.") SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )") SQL1.ExecNonQuery("vacuum;") Else ' 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 Sub \ No newline at end of file diff --git a/jRDC_Multi.b4j.meta b/jRDC_Multi.b4j.meta index dbc74b4..67c3490 100644 --- a/jRDC_Multi.b4j.meta +++ b/jRDC_Multi.b4j.meta @@ -43,6 +43,6 @@ ModuleClosedNodes6= ModuleClosedNodes7= ModuleClosedNodes8= 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=Main,TimerLogs_Tick,533,0,Main,LogQueryPerformance,414,5,Main,LogServerError,459,0,Main,AppStart,244,0,Main,WriteQueryLogsBatch,512,1,Main,borraArribaDe15000Logs,617,0,Main,WriteErrorLogsBatch,602,1,Main,InitializeSQLiteDatabase,365,0,Main,Process_Globals,76,4,Cambios,Process_Globals,22,6 SelectedBuild=0 VisibleModules=3,4,13,1,10,11,14,2