mirror of
https://github.com/KeymonSoft/jRDC-Multi.git
synced 2026-04-17 12:56:23 +00:00
- feat(sqlite): Implementa optimización de SQLite (WAL e Índices) - fix(manager): Extiende el comando 'test' para verificar todos los pools de conexión configurados. - Mejoras al subsistema de logs y diagnóstico del servidor jRDC2-Multi. - Cambios principales: 1. Optimización del Rendimiento de SQLite (users.db): * Habilitación de WAL: Se implementó PRAGMA journal_mode=WAL y PRAGMA synchronous=NORMAL en `InitializeSQLiteDatabase`. Esto reduce la contención de disco y mejora el rendimiento de I/O en las escrituras transaccionales de logs por lotes. * Índices de logs: Se agregaron índices a las columnas `timestamp` y `duration_ms` en `query_logs`, y a `timestamp` en `errores`. Esto acelera drásticamente las operaciones de limpieza periódica (`borraArribaDe15000Logs`) y la generación de reportes de consultas lentas (`slowqueries`). 2. Mejora del Comando de Diagnóstico 'test': * Se corrigió el comando `manager?command=test` para que no solo pruebe la conexión de `DB1`, sino que itere sobre `Main.listaDeCP` y fuerce la adquisición y liberación de una conexión (`GetConnection`) en *todos* los `RDCConnector` configurados (DB1, DB2, DB3, etc.). * La nueva lógica garantiza una prueba de vida rigurosa de cada pool C3P0, devolviendo un mensaje detallado del estado de conectividad y registrando un error crítico vía `LogServerError` si algún pool no responde.
780 lines
30 KiB
Plaintext
780 lines
30 KiB
Plaintext
AppType=StandardJava
|
|
Build1=Default,b4j.JRDCMulti
|
|
File1=config.DB2.properties
|
|
File2=config.DB3.properties
|
|
File3=config.DB4.properties
|
|
File4=config.properties
|
|
File5=reiniciaProcesoBow.bat
|
|
File6=reiniciaProcesoPM2.bat
|
|
File7=start.bat
|
|
File8=start2.bat
|
|
File9=stop.bat
|
|
FileGroup1=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=bcrypt
|
|
Library2=byteconverter
|
|
Library3=javaobject
|
|
Library4=jcore
|
|
Library5=jrandomaccessfile
|
|
Library6=jserver
|
|
Library7=jshell
|
|
Library8=json
|
|
Library9=jsql
|
|
Module1=Cambios
|
|
Module10=Manager
|
|
Module11=Manager0
|
|
Module12=ParameterValidationUtils
|
|
Module13=ping
|
|
Module14=RDCConnector
|
|
Module15=SSE
|
|
Module16=SSEHandler
|
|
Module17=TestHandler
|
|
Module2=ChangePassHandler
|
|
Module3=DBHandlerB4X
|
|
Module4=DBHandlerJSON
|
|
Module5=DoLoginHandler
|
|
Module6=faviconHandler
|
|
Module7=GlobalParameters
|
|
Module8=LoginHandler
|
|
Module9=LogoutHandler
|
|
NumberOfFiles=9
|
|
NumberOfLibraries=9
|
|
NumberOfModules=17
|
|
Version=10.3
|
|
@EndOfDesignText@
|
|
'Non-UI application (console / server application)
|
|
|
|
#Region Project Attributes
|
|
|
|
#CommandLineArgs:
|
|
#MergeLibraries: True
|
|
' VERSION 5.09.19
|
|
'###########################################################################################################
|
|
'###################### 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 LOG_CACHE_THRESHOLD As Int = 350 ' Umbral de registros para forzar la escritura
|
|
|
|
Dim logger As Boolean
|
|
|
|
Public LatestPoolStats As Map ' Mapa Thread-Safe para almacenar las últimas métricas de cada pool.
|
|
End Sub
|
|
|
|
Sub AppStart (Args() As String)
|
|
|
|
SSE.Initialize
|
|
#if DEBUG
|
|
logger = True
|
|
LOG_CACHE_THRESHOLD = 10
|
|
#else
|
|
logger = False
|
|
#End If
|
|
' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
|
|
|
|
' La subcarpeta es "www"
|
|
CopiarRecursoSiNoExiste("manager.html", "www")
|
|
CopiarRecursoSiNoExiste("login.html", "www")
|
|
|
|
' --- Copiar los archivos .bat de la raíz ---
|
|
' La subcarpeta es "" (vacía) porque están en la raíz de "Files"
|
|
CopiarRecursoSiNoExiste("config.properties", "")
|
|
CopiarRecursoSiNoExiste("config.DB2.properties", "")
|
|
CopiarRecursoSiNoExiste("config.DB3.properties", "")
|
|
CopiarRecursoSiNoExiste("start.bat", "")
|
|
CopiarRecursoSiNoExiste("start2.bat", "")
|
|
CopiarRecursoSiNoExiste("stop.bat", "")
|
|
CopiarRecursoSiNoExiste("reiniciaProcesoBow.bat", "")
|
|
CopiarRecursoSiNoExiste("reiniciaProcesoPM2.bat", "")
|
|
'
|
|
' Log("Verificación de archivos completada.")
|
|
|
|
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
|
|
LatestPoolStats = srvr.CreateThreadSafeMap ' Inicializar el mapa de estadísticas como Thread-Safe
|
|
|
|
' 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", 0).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("/stats-stream", "SSEHandler", 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)
|
|
|
|
SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;")
|
|
SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;")
|
|
|
|
' 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)
|
|
|
|
If logger Then Log("Creando índices de rendimiento en tablas de logs.")
|
|
|
|
' Índice en timestamp para limpieza rápida (DELETE/ORDER BY) en query_logs
|
|
SQL1.ExecNonQuery("CREATE INDEX idx_query_timestamp ON query_logs(timestamp)")
|
|
|
|
' Índice en duration_ms para la consulta 'slowqueries' (ORDER BY)
|
|
SQL1.ExecNonQuery("CREATE INDEX idx_query_duration ON query_logs(duration_ms)")
|
|
|
|
' Índice en timestamp para limpieza rápida de la tabla de errores
|
|
SQL1.ExecNonQuery("CREATE INDEX idx_error_timestamp ON errores(timestamp)")
|
|
|
|
Else
|
|
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
|
|
Log("Base de datos de usuarios cargada.")
|
|
SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;")
|
|
SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;")
|
|
|
|
' >>> 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. ---
|
|
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.")
|
|
' 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 )")
|
|
|
|
' 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;")
|
|
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
|
|
|
|
'Copiamos recursos del jar al directorio de la app
|
|
Sub CopiarRecursoSiNoExiste(NombreArchivo As String, SubCarpeta As String)
|
|
Dim DirDestino As String = File.Combine(File.DirApp, SubCarpeta)
|
|
|
|
If SubCarpeta <> "" And File.Exists(DirDestino, "") = False Then
|
|
File.MakeDir(DirDestino, "")
|
|
End If
|
|
|
|
Dim ArchivoDestino As String = File.Combine(DirDestino, NombreArchivo)
|
|
|
|
If File.Exists(DirDestino, NombreArchivo) = False Then
|
|
|
|
Dim RutaRecurso As String
|
|
If SubCarpeta <> "" Then
|
|
RutaRecurso = "Files/" & SubCarpeta & "/" & NombreArchivo
|
|
Else
|
|
RutaRecurso = "Files/" & NombreArchivo
|
|
End If
|
|
|
|
Dim classLoader As JavaObject = GetThreadContextClassLoader
|
|
Dim InStream As InputStream = classLoader.RunMethod("getResourceAsStream", Array(RutaRecurso))
|
|
|
|
If InStream.IsInitialized Then
|
|
Log($"Copiando recurso: '${RutaRecurso}'..."$)
|
|
|
|
' Llamamos a nuestra propia función de copiado manual
|
|
Dim OutStream As OutputStream = File.OpenOutput(DirDestino, NombreArchivo, False)
|
|
CopiarStreamManualmente(InStream, OutStream)
|
|
|
|
Log($"'${ArchivoDestino}' copiado correctamente."$)
|
|
Else
|
|
Log($"ERROR: No se pudo encontrar el recurso con la ruta interna: '${RutaRecurso}'"$)
|
|
End If
|
|
End If
|
|
End Sub
|
|
|
|
' No depende de ninguna librería extraña.
|
|
Sub CopiarStreamManualmente (InStream As InputStream, OutStream As OutputStream)
|
|
Try
|
|
Dim buffer(1024) As Byte
|
|
Dim len As Int
|
|
len = InStream.ReadBytes(buffer, 0, buffer.Length)
|
|
Do While len > 0
|
|
OutStream.WriteBytes(buffer, 0, len)
|
|
len = InStream.ReadBytes(buffer, 0, buffer.Length)
|
|
Loop
|
|
Catch
|
|
LogError(LastException)
|
|
End Try
|
|
|
|
InStream.Close
|
|
OutStream.Close
|
|
End Sub
|
|
|
|
' Función ayudante para obtener el Class Loader correcto.
|
|
Sub GetThreadContextClassLoader As JavaObject
|
|
Dim thread As JavaObject
|
|
thread = thread.InitializeStatic("java.lang.Thread").RunMethod("currentThread", Null)
|
|
Return thread.RunMethod("getContextClassLoader", Null)
|
|
End Sub
|