mirror of
https://github.com/KeymonSoft/jRDC-Multi.git
synced 2026-04-17 21:06:24 +00:00
- 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.
671 lines
26 KiB
Plaintext
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 |