Files
jRDC-Multi/jRDC_Multi.b4j
Jose Alberto Guerra Ugalde 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

671 lines
26 KiB
Plaintext

AppType=StandardJava
Build1=Default,b4j.JRDCMulti
File1=config.DB2.properties
File10=stop.bat
File2=config.DB3.properties
File3=config.DB4.properties
File4=config.properties
File5=login.html
File6=reiniciaProcesoBow.bat
File7=reiniciaProcesoPM2.bat
File8=start.bat
File9=start2.bat
FileGroup1=Default Group
FileGroup10=Default Group
FileGroup2=Default Group
FileGroup3=Default Group
FileGroup4=Default Group
FileGroup5=Default Group
FileGroup6=Default Group
FileGroup7=Default Group
FileGroup8=Default Group
FileGroup9=Default Group
Group=Default Group
Library1=byteconverter
Library2=javaobject
Library3=jcore
Library4=jrandomaccessfile
Library5=jserver
Library6=jshell
Library7=json
Library8=jsql
Library9=bcrypt
Module1=Cambios
Module10=Manager
Module11=ParameterValidationUtils
Module12=ping
Module13=RDCConnector
Module14=TestHandler
Module2=ChangePassHandler
Module3=DBHandlerB4X
Module4=DBHandlerJSON
Module5=DoLoginHandler
Module6=faviconHandler
Module7=GlobalParameters
Module8=LoginHandler
Module9=LogoutHandler
NumberOfFiles=10
NumberOfLibraries=9
NumberOfModules=14
Version=10.3
@EndOfDesignText@
'Non-UI application (console / server application)
#Region Project Attributes
#CommandLineArgs:
#MergeLibraries: True
' VERSION 5.09.16.2
'###########################################################################################################
'###################### PULL #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull
'###########################################################################################################
'###################### PUSH #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe&Args=github&Args=..\..\
'###########################################################################################################
'###################### PUSH TORTOISE GIT #########################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe&Args=TortoiseGitProc&Args=/command:commit&Args=/path:"../"&Args=/closeonend:2
'###########################################################################################################
#End Region
'change based on the jdbc jar file
'#AdditionalJar: mysql-connector-java-5.1.27-bin
'#AdditionalJar: postgresql-42.7.0
#AdditionalJar: ojdbc11
' Librería para manejar la base de datos SQLite
#AdditionalJar: sqlite-jdbc-3.7.2
Sub Process_Globals
' --- Variables globales accesibles desde cualquier parte del proyecto ---
' Objeto principal del servidor HTTP de B4J.
Public srvr As Server
' La versión actual de este servidor jRDC modificado.
Public const VERSION As Float = 2.23
' Tipos personalizados (clases) para la serialización y deserialización de datos
Type DBCommand (Name As String, Parameters() As Object)
Type DBResult (Tag As Object, Columns As Map, Rows As List)
' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2").
Public listaDeCP As List
' Una lista temporal para almacenar los nombres de archivos de configuración encontrados.
Private cpFiles As List
' Mapas globales para gestionar los conectores de base de datos y los comandos SQL.
Public Connectors, commandsMap As Map
' Objeto SQL para interactuar con la base de datos de usuarios y logs (SQLite).
Public SQL1 As SQL
' Objeto para realizar operaciones de hashing de contraseñas de forma segura.
Private bc As BCrypt
' Objeto de bloqueo (ReentrantLock) para proteger Main.Connectors durante Hot-Swap.
Public MainConnectorsLock As JavaObject
' Timer para ejecutar tareas periódicas, como la limpieza de logs.
Public timerLogs As Timer
' 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
' Tipo para encapsular el resultado de la validación de parámetros.
Type ParameterValidationResult ( _
Success As Boolean, _
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
' 2. Inicializa los mapas globales definidos en GlobalParameters.bas.
GlobalParameters.mpLogs.Initialize
GlobalParameters.mpTotalRequests.Initialize
GlobalParameters.mpTotalConnections.Initialize
GlobalParameters.mpBlockConnection.Initialize
' Aseguramos que el mapa de conteo de peticiones activas sea thread-safe.
GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap
' 3. Inicializa las estructuras principales del servidor HTTP.
listaDeCP.Initialize
srvr.Initialize("")
Connectors = srvr.CreateThreadSafeMap
commandsMap.Initialize
' NUEVO: Inicializar el mapa de estado de logs granular
SQLiteLoggingStatusByDB.Initialize
' Creamos una instancia de ReentrantLock para proteger Main.Connectors.
MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null)
' === 4. INICIALIZACIÓN DEL CONECTOR PARA LA BASE DE DATOS PRINCIPAL (DB1) ===
Try
Dim con1 As RDCConnector
con1.Initialize("DB1")
Connectors.Put("DB1", con1)
srvr.Port = con1.serverPort
listaDeCP.Add("DB1")
Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$)
' Lógica de Logs para DB1 (Fuente principal de configuración)
Dim enableLogsSetting As Int = con1.config.GetDefault("enableSQLiteLogs", 1).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put("DB1", isEnabled) ' Guardar el estado
Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR CRÍTICO al inicializar conector 'DB1': ${LastException.Message}"$
Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB1", Null, Null)
ExitApplication
End Try
' === 5. DETECCIÓN E INICIALIZACIÓN DE BASES DE DATOS ADICIONALES (DB2, DB3, DB4) ===
cpFiles = File.ListFiles("./")
If cpFiles.Size > 0 Then
For i = 0 To cpFiles.Size - 1
' Procesa 'config.DB2.properties'
If cpFiles.Get(i) = "config.DB2.properties" Then
Try
Dim con2 As RDCConnector
con2.Initialize("DB2")
Connectors.Put("DB2", con2)
listaDeCP.Add("DB2")
Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.")
' Lógica de Logs para DB2
Dim enableLogsSetting As Int = con2.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put("DB2", isEnabled) ' Guardar el estado
Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB2': ${LastException.Message}"$
Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB2", Null, Null)
End Try
End If
' Procesa 'config.DB3.properties'
If cpFiles.Get(i) = "config.DB3.properties" Then
Try
Dim con3 As RDCConnector
con3.Initialize("DB3")
Connectors.Put("DB3", con3)
listaDeCP.Add("DB3")
Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.")
' Lógica de Logs para DB3
Dim enableLogsSetting As Int = con3.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put("DB3", isEnabled) ' Guardar el estado
Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB3': ${LastException.Message}"$
Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB3", Null, Null)
End Try
End If
' Procesa 'config.DB4.properties'
If cpFiles.Get(i) = "config.DB4.properties" Then
Try
Dim con4 As RDCConnector
con4.Initialize("DB4")
Connectors.Put("DB4", con4)
listaDeCP.Add("DB4")
Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.")
' Lógica de Logs para DB4
Dim enableLogsSetting As Int = con4.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put("DB4", isEnabled) ' Guardar el estado
Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB4': ${LastException.Message}"$
Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB4", Null, Null)
End Try
End If
Next
Else
Log("Main.AppStart: No se encontraron archivos de configuración adicionales (config.DBx.properties).")
End If
' Log final de las bases de datos que el servidor está gestionando.
Dim sbListaDeCP_Log As StringBuilder
sbListaDeCP_Log.Initialize
For Each item As String In listaDeCP
sbListaDeCP_Log.Append(item).Append(", ")
Next
If sbListaDeCP_Log.Length > 0 Then
sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length)
End If
Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$)
' <<<< Bloque de inicialización del Timer para la limpieza de logs >>>>
' Inicialización INCONDICIONAL del Timer (Garantiza que el objeto exista y prevenga el IllegalStateException)
timerLogs.Initialize("TimerLogs", 600000) ' 10 minutos = 600 * 1000 = 600000 ms
' CONTROL CONDICIONAL BASADO EN EL ESTADO GRANULAR
IsAnySQLiteLoggingEnabled = False
For Each dbStatus As Boolean In SQLiteLoggingStatusByDB.Values
If dbStatus Then
IsAnySQLiteLoggingEnabled = True
Exit ' Si uno está activo, es suficiente para encender el Timer
End If
Next
If IsAnySQLiteLoggingEnabled Then
timerLogs.Enabled = True
If logger Then Log("Main.AppStart: Timer de limpieza de logs ACTIVADO (al menos una DB requiere logs).")
Else
timerLogs.Enabled = False
If logger Then Log("Main.AppStart: Timer de limpieza de logs DESHABILITADO (ninguna DB requiere logs).")
End If
' <<<< Fin del bloque del Timer >>>>
' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR ===
srvr.AddHandler("/ping", "ping", False)
srvr.AddHandler("/test", "TestHandler", False)
srvr.AddHandler("/login", "LoginHandler", False)
srvr.AddHandler("/dologin", "DoLoginHandler", False)
srvr.AddHandler("/logout", "LogoutHandler", False)
srvr.AddHandler("/changepass", "ChangePassHandler", False)
srvr.AddHandler("/manager", "Manager", False)
srvr.AddHandler("/DBJ", "DBHandlerJSON", False)
srvr.AddHandler("/dbrquery", "DBHandlerJSON", False)
srvr.AddHandler("/favicon.ico", "faviconHandler", False)
srvr.AddHandler("/*", "DBHandlerB4X", False)
' 7. Inicia el servidor HTTP.
srvr.Start
Log("===========================================================")
Log($"-=== jRDC está funcionando en el puerto: ${srvr.Port} (versión = $1.2{VERSION}) ===-"$)
Log("===========================================================")
' 8. Inicia el bucle de mensajes de B4J.
StartMessageLoop
End Sub
' --- Subrutina para inicializar la base de datos de usuarios local (SQLite) ---
Sub InitializeSQLiteDatabase
Dim dbFileName As String = "users.db"
If File.Exists(File.DirApp, dbFileName) = False Then
Log("Creando nueva base de datos de usuarios: " & dbFileName)
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
' Crear tabla 'users'
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
SQL1.ExecNonQuery(createUserTable)
' Crear tabla 'query_logs'
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)
' Insertar usuario por defecto
Dim defaultUser As String = "admin"
Dim defaultPass As String = "12345"
Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt)
SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass))
Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$)
' Crear tabla 'errores'
Log("Creando tabla 'errores' para registrar eventos.")
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
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
Log("Base de datos de usuarios cargada.")
' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<<
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 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)"
SQL1.ExecNonQuery(createQueryLogsTable)
Else
' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes (busy_connections, handler_active_requests).
Dim columnExists As Boolean
Dim rs As ResultSet
' --- VERIFICAR Y AÑADIR busy_connections ---
columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("busy_connections") 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 'busy_connections' a query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0")
End If
' --- VERIFICAR Y AÑADIR handler_active_requests ---
columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("handler_active_requests") 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 '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 <<<
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 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
If logger Then Log("Tabla 'errores' ya existe.")
End If
' >>> FIN: Lógica de migración para 'errores' <<<
End If
' >>> FIN: Lógica de migración (ALTER TABLE) <<<
End If
End Sub
Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int)
Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DbKey, False)
If isEnabled Then
' 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'. ---
Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String)
Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DBKey, False)
If isEnabled Then
' 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)
LogServerError("ERROR", "Main.TimerLogs_Tick", ErrorMsg, Null, "log_cleanup", Null)
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.
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.
If logger Then Log("AVISO: Tarea de limpieza de logs omitida. El logging global de SQLite está deshabilitado.")
End If
End Sub