diff --git a/Cambios.bas b/Cambios.bas
index 364d906..5adff58 100644
--- a/Cambios.bas
+++ b/Cambios.bas
@@ -21,15 +21,17 @@ Sub Process_Globals
' los dominios permitidos.
' - Ej: token:1224abcd5678fghi, dominio:"keymon.net"
' - Ej: token:4321abcd8765fghi, dominio:"*"
-' - Que los logs, en lugar de guardar de uno en uno en la BD Sqlte, se guarden en memoria, se junten ... por ejemplo 100 y ya que haya 100, se guarden
-' en una solo query a la BD Sqlite.
' - 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.17
+' - VERSION 5.09.18
+' - feat(manager): Implementa recarga granular (Hot-Swap).
+' - Actualiza manager.html para solicitar la DB Key a recargar (ej: DB2).
+' - Se modifica Manager.bas para leer este parámetro y ejecutar el Hot-Swap de forma atómica solo en el pool de conexión especificado, lo cual mejora la eficiencia y la disponibilidad del servicio.
- ' - fix(handlers, logs): Reporte robusto de AffectedRows (simbólico) y limpieza de tabla de errores
- ' - Aborda dos problemas críticos para la estabilidad y fiabilidad del servidor: el manejo del conteo de filas afectadas en DMLs y la gestión del crecimiento de la tabla de logs de errores.
+' - VERSION 5.09.17
+' - fix(handlers, logs): Reporte robusto de AffectedRows (simbólico) y limpieza de tabla de errores
+' - Aborda dos problemas críticos para la estabilidad y fiabilidad del servidor: el manejo del conteo de filas afectadas en DMLs y la gestión del crecimiento de la tabla de logs de errores.
' - Cambios Principales:
diff --git a/DBHandlerB4X.bas b/DBHandlerB4X.bas
index f7d804b..3df853a 100644
--- a/DBHandlerB4X.bas
+++ b/DBHandlerB4X.bas
@@ -105,17 +105,34 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
con = Connector.GetConnection(dbKey) ' ¡La conexión a la BD se obtiene aquí del pool de conexiones!
- ' <<<< ¡BUSY_CONNECTIONS YA SE CAPTURABA BIEN! >>>>
' Este bloque captura el número de conexiones actualmente ocupadas en el pool
' *después* de que esta petición ha obtenido la suya.
If Connector.IsInitialized Then
Dim poolStats As Map = Connector.GetPoolStats
If poolStats.ContainsKey("BusyConnections") Then
- ' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>>
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor.
+ Log($">>>>>>>>>> ${poolStats.Get("BusyConnections")} "$)
End If
End If
- ' <<<< ¡FIN DE CAPTURA! >>>>
+
+ Dim cachedStatsB4X As Map = Main.LatestPoolStats.Get(dbKey).As(Map)
+
+ If cachedStatsB4X.IsInitialized Then
+ ' 1. Actualizar Busy Connections y Active Requests
+ cachedStatsB4X.Put("BusyConnections", poolBusyConnectionsForLog)
+ cachedStatsB4X.Put("HandlerActiveRequests", requestsBeforeDecrement)
+
+ ' 2. Capturar TotalConnections y IdleConnections (ya disponibles en poolStats)
+ If poolStats.ContainsKey("TotalConnections") Then
+ cachedStatsB4X.Put("TotalConnections", poolStats.Get("TotalConnections"))
+ End If
+ If poolStats.ContainsKey("IdleConnections") Then
+ cachedStatsB4X.Put("IdleConnections", poolStats.Get("IdleConnections"))
+ End If
+
+ ' 3. Re-escribir el mapa en el cache global (es Thread-Safe)
+ Main.LatestPoolStats.Put(dbKey, cachedStatsB4X)
+ End If
' Log("Metodo: " & method) ' Log de depuración para identificar el método de la petición.
@@ -167,9 +184,16 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
End If
Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL ---
- Log(LastException) ' Registra la excepción completa en el log.
- Main.LogServerError("ERROR", "DBHandlerB4X.Handle", LastException.Message, dbKey, q, req.RemoteAddress) ' <-- Nuevo Log
- SendPlainTextError(resp, 500, LastException.Message) ' Envía un error 500 al cliente.
+ Dim errorMessage As String = LastException.Message
+ If errorMessage.Contains("ORA-01002") Or errorMessage.Contains("recuperación fuera de secuencia") Then
+ errorMessage = "SE USA EXECUTEQUERY EN LUGAR DE EXECUTECOMMAND: " & errorMessage
+ else If errorMessage.Contains("ORA-17003") Or errorMessage.Contains("Índice de columnas no válido") Then
+ errorMessage = "NUMERO DE PARAMETROS EQUIVOCADO: " & errorMessage
+ End If
+
+ Log(errorMessage) ' Registra la excepción completa en el log.
+ Main.LogServerError("ERROR", "DBHandlerB4X.Handle", errorMessage, dbKey, q, req.RemoteAddress) ' <-- Nuevo Log
+ SendPlainTextError(resp, 500, errorMessage) ' Envía un error 500 al cliente.
q = "error_in_b4x_handler" ' Aseguramos un valor para 'q' en caso de excepción.
End Try ' --- FIN: Bloque Try principal ---
@@ -344,11 +368,12 @@ Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As S
Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in))
' Obtiene la lista de objetos DBCommand.
Dim commands As List = m.Get("commands")
+ Dim totalAffectedRows As Int = 0 ' Contador para acumular el total de filas afectadas.
' Prepara un objeto DBResult para la respuesta (aunque para batch no devuelve datos, solo confirmación).
Dim res As DBResult
res.Initialize
- res.columns = CreateMap("AffectedRows (N/A)": 0) ' Columna simbólica.
+ res.columns = CreateMap("AffectedRows": 0) ' Columna simbólica.
res.Rows.Initialize
res.Tag = Null
@@ -390,10 +415,14 @@ Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As S
End If
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta el comando con la lista de parámetros validada.
+
+ totalAffectedRows = totalAffectedRows + 1 ' Acumulamos 1 por cada comando ejecutado sin error.
+
' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH >>>
+
Next
- res.Rows.Add(Array As Object(0)) ' Añade una fila simbólica al resultado para indicar éxito.
+ res.Rows.Add(Array As Object(totalAffectedRows)) ' Añade una fila simbólica al resultado para indicar éxito.
con.TransactionSuccessful ' Si todos los comandos se ejecutaron sin error, confirma la transacción.
Catch
' Si cualquier comando falla, se captura el error.
@@ -427,7 +456,7 @@ End Sub
' Ejecuta un lote de comandos usando el protocolo V1.
Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
- Log($"ExecuteBatch ${DB}"$)
+' Log($"ExecuteBatch ${DB}"$)
' Lee y descarta la versión del cliente.
Dim clientVersion As Float = ReadObject(in) 'ignore
' Lee cuántos comandos vienen en el lote.
@@ -441,18 +470,18 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se
Try
con.BeginTransaction
' Itera para procesar cada comando del lote.
- Log(numberOfStatements)
+' Log(numberOfStatements)
For i = 0 To numberOfStatements - 1
- Log($"i: ${i}"$)
+' Log($"i: ${i}"$)
' 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)
- Log(params)
+' Log(params)
If numberOfStatements = 1 Then
singleQueryName = queryName 'Capturamos el nombre del query.
End If
Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
- Log(sqlCommand)
+' Log(sqlCommand)
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>>
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
con.Rollback ' Deshace la transacción si un comando es inválido.
@@ -473,7 +502,7 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se
Return "error" ' Salida temprana si la validación falla.
End If
- Log(validationResult.ParamsToExecute)
+' Log(validationResult.ParamsToExecute)
Dim affectedCount As Int = 1 ' Asumimos éxito (1) ya que la llamada directa es la única que ejecuta el SQL sin fallar en runtime.
@@ -486,14 +515,14 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se
con.TransactionSuccessful ' Confirma la transacción.
- Log("Transaction succesfull")
+' Log("Transaction succesfull")
Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime la salida antes de enviarla.
' Escribe la respuesta usando el serializador V1.
WriteObject(Main.VERSION, out)
WriteObject("batch", out)
WriteInt(res.Length, out)
- Log(affectedCounts.Size)
+' Log(affectedCounts.Size)
For Each r As Int In affectedCounts
WriteInt(r, out)
Next
diff --git a/DBHandlerJSON.bas b/DBHandlerJSON.bas
index 0dce2de..287a061 100644
--- a/DBHandlerJSON.bas
+++ b/DBHandlerJSON.bas
@@ -128,9 +128,29 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
If poolStats.ContainsKey("BusyConnections") Then
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>>
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor.
+' Log($">>>>>>>>>> ${poolStats.Get("BusyConnections")} "$)
End If
End If
' <<<< ¡FIN DE CAPTURA! >>>>
+
+ Dim cachedStatsJSON As Map = Main.LatestPoolStats.Get(finalDbKey).As(Map)
+
+ If cachedStatsJSON.IsInitialized Then
+ ' Los valores ya fueron capturados: poolBusyConnectionsForLog y requestsBeforeDecrement
+ cachedStatsJSON.Put("BusyConnections", poolBusyConnectionsForLog)
+ cachedStatsJSON.Put("HandlerActiveRequests", requestsBeforeDecrement)
+ If poolStats.ContainsKey("TotalConnections") Then
+ cachedStatsJSON.Put("TotalConnections", poolStats.Get("TotalConnections"))
+ End If
+ If poolStats.ContainsKey("IdleConnections") Then
+ cachedStatsJSON.Put("IdleConnections", poolStats.Get("IdleConnections"))
+ End If
+ ' Re-escribir el mapa en el cache global (es Thread-Safe)
+ Main.LatestPoolStats.Put(finalDbKey, cachedStatsJSON)
+' Log(Main.LatestPoolStats)
+ End If
+
+' Log($"Total: ${poolStats.Get("TotalConnections")}, Idle: ${poolStats.Get("IdleConnections")}, busy: ${poolBusyConnectionsForLog}, active: ${requestsBeforeDecrement}"$)
' Obtiene la sentencia SQL correspondiente al nombre del comando desde config.properties.
Dim sqlCommand As String = Connector.GetCommand(finalDbKey, queryNameForLog)
diff --git a/Files/www/manager.html b/Files/www/manager.html
new file mode 100644
index 0000000..ab8a90f
--- /dev/null
+++ b/Files/www/manager.html
@@ -0,0 +1,391 @@
+
+
+
+
+
+ jRDC2-Multi - Panel de Administración
+
+
+
+
+
+
+
Bienvenido
+
+
+ Selecciona una opción del menú para comenzar.
+
+
+
+
+
+
+
diff --git a/Manager.bas b/Manager.bas
index 4a99896..4a497ab 100644
--- a/Manager.bas
+++ b/Manager.bas
@@ -30,7 +30,8 @@ End Sub
' Método principal que maneja las peticiones HTTP para el panel de administración.
' Refactorizado para funcionar como una API con un frontend estático.
Sub Handle(req As ServletRequest, resp As ServletResponse)
- ' --- 1. Bloque de Seguridad (sin cambios) ---
+
+ ' --- 1. Bloque de Seguridad ---
If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
resp.SendRedirect("/login")
Return
@@ -39,7 +40,6 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Dim Command As String = req.GetParameter("command")
' --- 2. Servidor de la Página Principal ---
- ' Si NO se especifica un comando, servimos la página principal del manager desde la carpeta 'www'.
If Command = "" Then
Try
resp.ContentType = "text/html; charset=utf-8"
@@ -49,18 +49,15 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
End Try
Return
End If
-
+
' --- 3. Manejo de Comandos como API ---
- ' La variable 'j' (JSONGenerator) está en Class_Globals
-
Select Command.ToLowerCase
- ' --- Comandos que devuelven JSON ---
- Case "getstats"
+ ' --- Comandos que devuelven JSON (Métricas del Pool) ---
+ Case "getstatsold"
resp.ContentType = "application/json; charset=utf-8"
Dim allPoolStats As Map
allPoolStats.Initialize
-
For Each dbKey As String In Main.listaDeCP
Dim connector As RDCConnector = Main.Connectors.Get(dbKey)
If connector.IsInitialized Then
@@ -69,6 +66,22 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
End If
Next
+ j.Initialize(allPoolStats)
+ resp.Write(j.ToString)
+ Return
+
+ Case "getstats"
+ resp.ContentType = "application/json; charset=utf-8"
+ Dim allPoolStats As Map
+
+ ' Leemos del caché global actualizado por el Timer SSE
+ allPoolStats = Main.LatestPoolStats
+
+ For Each dbKey As String In Main.listaDeCP
+ If allPoolStats.ContainsKey(dbKey) = False Then
+ allPoolStats.Put(dbKey, CreateMap("Error": "Métricas no disponibles/Pool no inicializado"))
+ End If
+ Next
j.Initialize(allPoolStats)
resp.Write(j.ToString)
@@ -78,17 +91,18 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
resp.ContentType = "application/json; charset=utf-8"
Dim results As List
results.Initialize
+
Try
- ' Verificamos si la tabla de logs existe antes de consultarla
+ ' Verifica la existencia de la tabla de logs antes de consultar
Dim tableExists As Boolean = Main.SQL1.ExecQuerySingleResult($"SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs';"$) <> Null
+
If tableExists = False Then
- ' Si la tabla no existe, devolvemos un JSON con un mensaje claro y terminamos.
j.Initialize(CreateMap("message": "La tabla de logs ('query_logs') no existe. Habilita 'enableSQLiteLogs=1' en la configuración."))
resp.Write(j.ToString)
Return
End If
-
- ' La tabla existe, procedemos con la consulta original
+
+ ' Consulta las 20 queries más lentas de la última hora
Dim oneHourAgoMs As Long = DateTime.Now - 3600000
Dim rs As ResultSet = Main.SQL1.ExecQuery($"SELECT query_name, duration_ms, datetime(timestamp / 1000, 'unixepoch', 'localtime') as timestamp_local, db_key, client_ip, busy_connections, handler_active_requests FROM query_logs WHERE timestamp >= ${oneHourAgoMs} ORDER BY duration_ms DESC LIMIT 20"$)
@@ -106,40 +120,31 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Loop
rs.Close
- ' 1. Creamos un mapa "raíz" para contener nuestra lista.
Dim root As Map
root.Initialize
- root.Put("data", results) ' La llave puede ser lo que quieras, "data" es común.
-
- ' 2. Ahora sí, inicializamos el generador con el mapa raíz.
+ root.Put("data", results)
j.Initialize(root)
resp.Write(j.ToString)
-
+
Catch
Log("Error CRÍTICO al obtener queries lentas en Manager API: " & LastException.Message)
-
- ' <<< CORRECCIÓN AQUÍ >>>
- ' Se utiliza la propiedad .Status para asignar el código de error
- resp.Status = 500 ' Internal Server Error
-
- ' 1. Creamos un mapa "raíz" para contener nuestra lista.
+ resp.Status = 500
+
Dim root As Map
root.Initialize
- root.Put("data", results) ' La llave puede ser lo que quieras, "data" es común.
-
- ' 2. Ahora sí, inicializamos el generador con el mapa raíz.
+ root.Put("data", results)
j.Initialize(root)
resp.Write(j.ToString)
End Try
Return
-
+
Case "logs", "totalrequests", "totalblocked"
resp.ContentType = "application/json; charset=utf-8"
Dim mp As Map
If Command = "logs" And GlobalParameters.mpLogs.IsInitialized Then mp = GlobalParameters.mpLogs
If Command = "totalrequests" And GlobalParameters.mpTotalRequests.IsInitialized Then mp = GlobalParameters.mpTotalRequests
If Command = "totalblocked" And GlobalParameters.mpBlockConnection.IsInitialized Then mp = GlobalParameters.mpBlockConnection
-
+
If mp.IsInitialized Then
j.Initialize(mp)
resp.Write(j.ToString)
@@ -147,168 +152,216 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
resp.Write("{}")
End If
Return
-
+
' --- Comandos que devuelven TEXTO PLANO ---
Case "ping"
resp.ContentType = "text/plain"
resp.Write($"Pong ($DateTime{DateTime.Now})"$)
Return
-
+
Case "reload"
resp.ContentType = "text/plain; charset=utf-8"
- Dim sbTemp As StringBuilder
- sbTemp.Initialize
+ Dim sbTemp As StringBuilder
+ sbTemp.Initialize
- ' <<< LÓGICA ORIGINAL: Se mantiene intacta toda la lógica de recarga >>>
- ' (Copiada y pegada directamente de tu código anterior)
- sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF)
- Dim oldTimerState As Boolean = Main.timerLogs.Enabled
- If oldTimerState Then
- Main.timerLogs.Enabled = False
- sbTemp.Append(" -> Timer de limpieza de logs (SQLite) detenido temporalmente.").Append(" " & CRLF)
- End If
- Dim newConnectors As Map
- newConnectors.Initialize
- Dim oldConnectors As Map
- Dim reloadSuccessful As Boolean = True
- Main.MainConnectorsLock.RunMethod("lock", Null)
- oldConnectors = Main.Connectors
- Main.MainConnectorsLock.RunMethod("unlock", Null)
+ ' ***** LÓGICA DE RECARGA GRANULAR/SELECTIVA *****
+ Dim dbKeyToReload As String = req.GetParameter("db").ToUpperCase ' Leer parámetro 'db' opcional (ej: /manager?command=reload&db=DB3)
+ Dim targets As List ' Lista de DBKeys a recargar.
+ targets.Initialize
- For Each dbKey As String In Main.listaDeCP
- Try
- Dim newRDC As RDCConnector
- newRDC.Initialize(dbKey)
- Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0)
- Dim isEnabled As Boolean = (enableLogsSetting = 1)
- newConnectors.Put(dbKey & "_LOG_STATE", isEnabled)
- sbTemp.Append($" -> Logs de ${dbKey} activados: ${isEnabled}"$).Append(" " & CRLF)
- newConnectors.Put(dbKey, newRDC)
- Dim newPoolStats As Map = newRDC.GetPoolStats
- sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append(" " & CRLF)
- Catch
- sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF)
- reloadSuccessful = False
- Exit
- End Try
- Next
+ ' 1. Determinar el alcance de la recarga (selectiva o total)
+ If dbKeyToReload.Length > 0 Then
+ ' Recarga selectiva
+ If Main.listaDeCP.IndexOf(dbKeyToReload) = -1 Then
+ resp.Write($"ERROR: DBKey '${dbKeyToReload}' no es válida o no está configurada."$)
+ Return
+ End If
+ targets.Add(dbKeyToReload)
+ sbTemp.Append($"Iniciando recarga selectiva de ${dbKeyToReload} (Hot-Swap)..."$).Append(" " & CRLF)
+ Else
+ ' Recarga completa (comportamiento por defecto)
+ targets.AddAll(Main.listaDeCP)
+ sbTemp.Append($"Iniciando recarga COMPLETA de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF)
+ End If
- If reloadSuccessful Then
- Main.MainConnectorsLock.RunMethod("lock", Null)
- Main.Connectors = newConnectors
- Main.MainConnectorsLock.RunMethod("unlock", Null)
- Main.SQLiteLoggingStatusByDB.Clear
- Dim isAnyEnabled As Boolean = False
- For Each dbKey As String In Main.listaDeCP
- Dim isEnabled As Boolean = newConnectors.Get(dbKey & "_LOG_STATE")
- Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled)
- If isEnabled Then isAnyEnabled = True
- Next
- Main.IsAnySQLiteLoggingEnabled = isAnyEnabled
- If Main.IsAnySQLiteLoggingEnabled Then
- Main.timerLogs.Enabled = True
- sbTemp.Append($" -> Logs de SQLite HABILITADOS (Granular). Timer de limpieza ACTIVADO."$).Append(" " & CRLF)
- Else
- Main.timerLogs.Enabled = False
- sbTemp.Append($" -> Logs de SQLite DESHABILITADOS (Total). Timer de limpieza PERMANECERÁ DETENIDO."$).Append(" " & CRLF)
- End If
- sbTemp.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append(" " & CRLF)
- Else
- If oldTimerState Then
- Main.timerLogs.Enabled = True
- sbTemp.Append(" -> Restaurando Timer de limpieza de logs (SQLite) al estado ACTIVO debido a fallo en recarga.").Append(" " & CRLF)
- End If
- sbTemp.Append($"¡ERROR: La recarga de configuración falló! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
- End If
+ ' 2. Deshabilitar el Timer de logs (si es necesario)
+ Dim oldTimerState As Boolean = Main.timerLogs.Enabled
+ If oldTimerState Then
+ Main.timerLogs.Enabled = False
+ sbTemp.Append(" -> Timer de limpieza de logs (SQLite) detenido temporalmente.").Append(" " & CRLF)
+ End If
+
+ Dim reloadSuccessful As Boolean = True
+ Dim oldConnectorsToClose As Map ' Guardaremos los conectores antiguos aquí.
+ oldConnectorsToClose.Initialize
+
+ ' 3. Procesar solo los conectores objetivos
+ For Each dbKey As String In targets
+ sbTemp.Append($" -> Procesando recarga de ${dbKey}..."$).Append(CRLF)
+ Dim newRDC As RDCConnector
+
+ Try
+ ' Crear el nuevo conector con la configuración fresca
+ newRDC.Initialize(dbKey)
+
+ ' Adquirimos el lock para el reemplazo atómico
+ Main.MainConnectorsLock.RunMethod("lock", Null)
+
+ ' Guardamos el conector antiguo (si existe)
+ Dim oldRDC As RDCConnector = Main.Connectors.Get(dbKey)
+
+ ' Reemplazo atómico en el mapa global compartido
+ Main.Connectors.Put(dbKey, newRDC)
+
+ ' Liberamos el bloqueo inmediatamente
+ Main.MainConnectorsLock.RunMethod("unlock", Null)
+
+ ' Si había un conector antiguo, lo guardamos para cerrarlo después
+ If oldRDC.IsInitialized Then
+ oldConnectorsToClose.Put(dbKey, oldRDC)
+ End If
- ' <<< CAMBIO: Se devuelve el contenido del StringBuilder como texto plano >>>
- resp.Write(sbTemp.ToString)
- Return
-
- Case "test"
+ ' 4. Actualizar el estado de logs (Granular)
+ Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0)
+ Dim isEnabled As Boolean = (enableLogsSetting = 1)
+ Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled)
+ sbTemp.Append($" -> ${dbKey} recargado. Logs (config): ${isEnabled}"$).Append(CRLF)
+
+ Catch
+ ' Si falla la inicialización del pool, no actualizamos Main.Connectors
+
+ ' ¡CRÍTICO! Aseguramos que el lock se libere si hubo excepción antes de liberar.
+ If Main.MainConnectorsLock.RunMethod("isHeldByCurrentThread", Null).As(Boolean) Then
+ Main.MainConnectorsLock.RunMethod("unlock", Null)
+ End If
+
+ sbTemp.Append($" -> ERROR CRÍTICO al inicializar conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF)
+ reloadSuccessful = False
+ Exit
+ End Try
+ Next
+
+ ' 5. Cerrar los pools antiguos liberados (FUERA del Lock)
+ If reloadSuccessful Then
+ For Each dbKey As String In oldConnectorsToClose.Keys
+ Dim oldRDC As RDCConnector = oldConnectorsToClose.Get(dbKey)
+ oldRDC.Close ' Cierre limpio del pool C3P0
+ sbTemp.Append($" -> Pool antiguo de ${dbKey} cerrado limpiamente."$).Append(" " & CRLF)
+ Next
+
+ ' 6. Re-evaluar el estado global de Logs (CRÍTICO: debe revisar TODAS las DBs)
+ Main.IsAnySQLiteLoggingEnabled = False
+
+ For Each dbKey As String In Main.listaDeCP
+ ' Revisamos el estado de log de CADA conector activo
+ If Main.SQLiteLoggingStatusByDB.GetDefault(dbKey, False) Then
+ Main.IsAnySQLiteLoggingEnabled = True
+ Exit
+ End If
+ Next
+
+ If Main.IsAnySQLiteLoggingEnabled Then
+ Main.timerLogs.Enabled = True
+ sbTemp.Append($" -> Timer de limpieza de logs ACTIVADO (estado global: HABILITADO)."$).Append(" " & CRLF)
+ Else
+ Main.timerLogs.Enabled = False
+ sbTemp.Append($" -> Timer de limpieza de logs DESHABILITADO (estado global: DESHABILITADO)."$).Append(" " & CRLF)
+ End If
+
+ sbTemp.Append($"¡Recarga de configuración completada con éxito!"$).Append(" " & CRLF)
+
+ Else
+
+ ' Si falló, restauramos el estado del timer anterior.
+ If oldTimerState Then
+ Main.timerLogs.Enabled = True
+ sbTemp.Append(" -> Restaurando Timer de limpieza de logs al estado ACTIVO debido a fallo en recarga.").Append(" " & CRLF)
+ End If
+ sbTemp.Append($"¡ERROR: La recarga de configuración falló! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
+ End If
+
+ resp.Write(sbTemp.ToString)
+ Return
+
+ Case "test"
resp.ContentType = "text/plain; charset=utf-8"
- Dim sb As StringBuilder
+ Dim sb As StringBuilder
sb.Initialize
- Try
- Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("")
+
+ Try
+ Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("")
sb.Append("Connection successful." & CRLF & CRLF)
- Dim estaDB As String = ""
+ Dim estaDB As String = ""
Log(Main.listaDeCP)
- For i = 0 To Main.listaDeCP.Size - 1
- If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i)
+ For i = 0 To Main.listaDeCP.Size - 1
+ If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i)
sb.Append($"Using config${estaDB}.properties"$ & CRLF)
- Next
- con.Close
+ Next
+ con.Close
resp.Write(sb.ToString)
- Catch
- resp.Write("Error fetching connection: " & LastException.Message)
- End Try
- Return
-
- Case "rsx", "rpm2", "revivebow", "restartserver"
- resp.ContentType = "text/plain; charset=utf-8"
- Dim batFile As String
- Select Command
- Case "rsx": batFile = "start.bat"
- Case "rpm2": batFile = "reiniciaProcesoPM2.bat"
- Case "reviveBow": batFile = "reiniciaProcesoBow.bat"
- Case "restartserver": batFile = "restarServer.bat"
- End Select
- Log($"Ejecutando ${File.DirApp}\${batFile}"$)
- Try
- Dim shl As Shell
- shl.Initialize("shl","cmd", Array("/c", File.DirApp & "\" & batFile & " " & Main.srvr.Port))
- shl.WorkingDirectory = File.DirApp
- shl.Run(-1)
- resp.Write($"Comando '${Command}' ejecutado. Script invocado: ${batFile}"$)
- Catch
- resp.Write($"Error al ejecutar el script para '${Command}': ${LastException.Message}"$)
- End Try
- Return
+ Catch
+ resp.Write("Error fetching connection: " & LastException.Message)
+ End Try
+ Return
+ Case "rsx", "rpm2", "revivebow", "restartserver"
+ resp.ContentType = "text/plain; charset=utf-8"
+ Dim batFile As String
+ Select Command
+ Case "rsx": batFile = "start.bat"
+ Case "rpm2": batFile = "reiniciaProcesoPM2.bat"
+ Case "reviveBow": batFile = "reiniciaProcesoBow.bat"
+ Case "restartserver": batFile = "restarServer.bat" ' Nota: este bat no estaba definido, se usó el nombre del comando
+ End Select
- Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$)
- sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$)
- Public shl As Shell
- shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoPM2.bat " & Main.srvr.Port))
- shl.WorkingDirectory = File.DirApp
- shl.Run(-1)
+ Log($"Ejecutando ${File.DirApp}\${batFile}"$)
+ Try
+ Dim shl As Shell
+ shl.Initialize("shl","cmd", Array("/c", File.DirApp & "\" & batFile & " " & Main.srvr.Port))
+ shl.WorkingDirectory = File.DirApp
+ shl.Run(-1)
+ resp.Write($"Comando '${Command}' ejecutado. Script invocado: ${batFile}"$)
+ Catch
+ resp.Write($"Error al ejecutar el script para '${Command}': ${LastException.Message}"$)
+ End Try
+ Return
- Case "paused", "continue"
+ Case "paused", "continue"
resp.ContentType = "text/plain; charset=utf-8"
- If Command = "paused" Then
- GlobalParameters.IsPaused = 1
- resp.Write("Servidor pausado.")
- Else
- GlobalParameters.IsPaused = 0
- resp.Write("Servidor reanudado.")
- End If
- Return
-
- Case "block", "unblock"
+ If Command = "paused" Then
+ GlobalParameters.IsPaused = 1
+ resp.Write("Servidor pausado.")
+ Else
+ GlobalParameters.IsPaused = 0
+ resp.Write("Servidor reanudado.")
+ End If
+ Return
+
+ Case "block", "unblock"
resp.ContentType = "text/plain; charset=utf-8"
- Dim ip As String = req.GetParameter("IP")
- If ip = "" Then
- resp.Write("Error: El parámetro IP es requerido.")
- Return
- End If
- If GlobalParameters.mpBlockConnection.IsInitialized Then
- If Command = "block" Then
- GlobalParameters.mpBlockConnection.Put(ip, ip)
- resp.Write($"IP bloqueada: ${ip}"$)
- Else
- GlobalParameters.mpBlockConnection.Remove(ip)
- resp.Write($"IP desbloqueada: ${ip}"$)
- End If
- Else
- resp.Write("Error: El mapa de bloqueo no está inicializado.")
- End If
- Return
+ Dim ip As String = req.GetParameter("IP")
+
+ If ip = "" Then
+ resp.Write("Error: El parámetro IP es requerido.")
+ Return
+ End If
- Case Else
- resp.ContentType = "text/plain; charset=utf-8"
- resp.SendError(404, $"Comando desconocido: '{Command}'"$)
- Return
+ If GlobalParameters.mpBlockConnection.IsInitialized Then
+ If Command = "block" Then
+ GlobalParameters.mpBlockConnection.Put(ip, ip)
+ resp.Write($"IP bloqueada: ${ip}"$)
+ Else
+ GlobalParameters.mpBlockConnection.Remove(ip)
+ resp.Write($"IP desbloqueada: ${ip}"$)
+ End If
+ Else
+ resp.Write("Error: El mapa de bloqueo no está inicializado.")
+ End If
+ Return
- End Select
+ Case Else
+ resp.ContentType = "text/plain; charset=utf-8"
+ resp.SendError(404, $"Comando desconocido: '{Command}'"$)
+ Return
+ End Select
End Sub
\ No newline at end of file
diff --git a/RDCConnector.bas b/RDCConnector.bas
index db398f4..9cde619 100644
--- a/RDCConnector.bas
+++ b/RDCConnector.bas
@@ -78,15 +78,18 @@ Public Sub Initialize(DB As String)
Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2)
Dim maxPoolSize As Int = config.GetDefault("MaxPoolSize", 5)
Dim acquireIncrement As Int = config.GetDefault("AcquireIncrement", 5)
+ Dim maxIdleTime As Int = config.GetDefault("MaxIdleTime", 300)
+ Dim maxConnectionAge As Int = config.GetDefault("MaxConnectionAge", 900)
+ Dim checkoutTimeout As Int = config.GetDefault("CheckoutTimeout", 60000)
' Configuración de los parámetros del pool de conexiones C3P0:
- jo.RunMethod("setInitialPoolSize", Array(initialPoolSize)) ' Define el número de conexiones que se intentarán crear al iniciar el pool.
- jo.RunMethod("setMinPoolSize", Array(minPoolSize)) ' Fija el número mínimo de conexiones que el pool mantendrá abiertas.
- jo.RunMethod("setMaxPoolSize", Array(maxPoolSize)) ' Define el número máximo de conexiones simultáneas.
- jo.RunMethod("setAcquireIncrement", Array(acquireIncrement)) ' Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
- jo.RunMethod("setMaxIdleTime", Array As Object(config.GetDefault("MaxIdleTime", 300))) ' Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
- jo.RunMethod("setMaxConnectionAge", Array As Object(config.GetDefault("MaxConnectionAge", 900))) ' Tiempo máximo de vida de una conexión (segundos).
- jo.RunMethod("setCheckoutTimeout", Array As Object(config.GetDefault("CheckoutTimeout", 60000))) ' Tiempo máximo de espera por una conexión del pool (milisegundos).
+ jo.RunMethod("setInitialPoolSize", Array(initialPoolSize)) ' Define el número de conexiones que se intentarán crear al iniciar el pool.
+ jo.RunMethod("setMinPoolSize", Array(minPoolSize)) ' Fija el número mínimo de conexiones que el pool mantendrá abiertas.
+ jo.RunMethod("setMaxPoolSize", Array(maxPoolSize)) ' Define el número máximo de conexiones simultáneas.
+ jo.RunMethod("setAcquireIncrement", Array(acquireIncrement)) ' Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
+ jo.RunMethod("setMaxIdleTime", Array As Object(maxIdleTime)) ' Es el tiempo máximo (en segundos) que una conexión puede permanecer inactiva en el pool antes de ser cerrada para ahorrar recursos.
+ jo.RunMethod("setMaxConnectionAge", Array As Object(maxConnectionAge)) ' Tiempo máximo de vida de una conexión (segundos), previene conexiones viciadas.
+ jo.RunMethod("setCheckoutTimeout", Array As Object(checkoutTimeout)) ' Tiempo máximo de espera por una conexión del pool (milisegundos).
' LÍNEAS CRÍTICAS PARA FORZAR UN COMPORTAMIENTO NO SILENCIOSO DE C3P0:
' Por defecto, C3P0 puede reintentar muchas veces y no lanzar una excepción si las conexiones iniciales fallan.
@@ -103,6 +106,16 @@ Public Sub Initialize(DB As String)
tempCon.Close ' Devolvemos la conexión inmediatamente al pool para que esté disponible.
End If
+ ' Cargar configuración estática en el cache global
+ Dim dbKeyToStore As String = DB
+ If dbKeyToStore = "" Then dbKeyToStore = "DB1" ' Aseguramos la llave si era DB1
+ Dim initialPoolStats As Map = GetPoolStats ' Llama a la función que usa JavaObject
+
+ ' PASO C: Almacenamos el mapa completo (estático + dinámico inicial) en el cache global.
+ Main.LatestPoolStats.Put(dbKeyToStore, initialPoolStats)
+
+ Log(Main.LatestPoolStats)
+
' com.mchange.v2.c3p0.ComboPooledDataSource [
' acquireIncrement -> 3,
' acquireRetryAttempts -> 30,
diff --git a/SSE.bas b/SSE.bas
new file mode 100644
index 0000000..95f4ec5
--- /dev/null
+++ b/SSE.bas
@@ -0,0 +1,187 @@
+B4J=true
+Group=Default Group
+ModulesStructureVersion=1
+Type=StaticCode
+Version=10.3
+@EndOfDesignText@
+' Módulo para gestionar conexiones y transmisiones de Server-Sent Events (SSE).
+
+' Declaración de variables globales a nivel de proceso.
+Sub Process_Globals
+ ' 'Connections' es un mapa (diccionario) para almacenar las conexiones SSE activas.
+ ' La clave será una combinación del 'path' y un GUID único, y el valor será el OutputStream de la respuesta.
+ ' Se usará un 'ThreadSafeMap' para evitar problemas de concurrencia entre hilos.
+ Dim Connections As Map
+
+
+ ' Timer #1 ("El Vigilante"): Se encarga de detectar y eliminar conexiones muertas.
+ Private RemoveTimer As Timer
+
+ ' Timer #2 ("El Informante"): Se encarga de recolectar y enviar los datos de estadísticas.
+ Dim StatsTimer As Timer
+ Dim const UPDATE_INTERVAL_MS As Long = 2000 ' Intervalo de envío de estadísticas: 2 segundos.
+End Sub
+
+' Subrutina de inicialización del módulo. Se llama una vez cuando el objeto es creado.
+public Sub Initialize()
+ ' Crea el mapa 'Connections' como un mapa seguro para hilos (ThreadSafeMap).
+ ' Esto es fundamental porque múltiples peticiones (hilos) pueden intentar agregar o remover conexiones simultáneamente.
+ Connections = Main.srvr.CreateThreadSafeMap
+
+ ' Inicializa el temporizador 'RemoveTimer' para que dispare el evento "RemoveTimer" cada 5000 milisegundos (5 segundos).
+ RemoveTimer.Initialize("RemoveTimer", 5000)
+
+ ' Habilita el temporizador para que comience a funcionar.
+ RemoveTimer.Enabled = True
+
+
+ Log("Stats SSE Handler Initialized (Singleton Mode)")
+
+ ' Crea el mapa de conexiones, asegurando que sea seguro para el manejo de múltiples hilos.
+ Connections = Main.srvr.CreateThreadSafeMap
+
+ ' Configura y activa el timer para la limpieza de conexiones cada 5 segundos.
+ ' NOTA: El EventName "RemoveTimer" debe coincidir con el nombre de la subrutina del tick.
+ RemoveTimer.Initialize("RemoveTimer", 5000)
+ RemoveTimer.Enabled = True
+
+ ' Configura y activa el timer para el envío de estadísticas.
+ ' NOTA: El EventName "StatsTimer" debe coincidir con el nombre de la subrutina del tick.
+ StatsTimer.Initialize("StatsTimer", UPDATE_INTERVAL_MS)
+ StatsTimer.Enabled = True
+End Sub
+
+' Subrutina para agregar un nuevo cliente (target) al stream de eventos SSE.
+' Se llama cuando un cliente se conecta al endpoint SSE.
+' Registra formalmente a un nuevo cliente en el sistema.
+Sub AddTarget(path As String, resp As ServletResponse)
+ ' Genera una clave única para esta conexión específica.
+ Dim connectionKey As String = path & "|" & GetGUID
+ Log("--- [SSE] Cliente conectado: " & connectionKey & " ---")
+
+ ' Configura las cabeceras HTTP necesarias para que el navegador mantenga la conexión abierta.
+ resp.ContentType = "text/event-stream"
+ resp.SetHeader("Cache-Control", "no-cache")
+ resp.SetHeader("Connection", "keep-alive")
+ resp.CharacterEncoding = "UTF-8"
+ resp.Status = 200
+
+ ' Añade al cliente y su canal de comunicación al mapa central.
+ Connections.Put(connectionKey, resp.OutputStream)
+ ' Envía un primer mensaje de bienvenida para confirmar la conexión.
+ SendMessage(resp.OutputStream, "open", "Connection established", 0, connectionKey)
+End Sub
+
+' Envía un mensaje a todos los clientes suscritos a un "path" específico.
+Sub Broadcast(Path As String, EventName As String, Message As String, Retry As Long)
+ ' Itera sobre la lista de clientes activos.
+ For Each key As String In Connections.Keys
+' Log(key)
+ ' Filtra para enviar solo a los clientes del path correcto (en este caso, "stats").
+ If key.StartsWith(Path & "|") Then
+ Try
+ ' Llama a la función de bajo nivel para enviar el mensaje formateado.
+ SendMessage(Connections.Get(key), EventName, Message, Retry, DateTime.Now)
+ Catch
+ ' Si el envío falla, asume que el cliente se desconectó y lo elimina.
+ Log("######################")
+ Log("## Removing (broadcast failed): " & key)
+ Log("######################")
+ Connections.Remove(key)
+ End Try
+ End If
+ Next
+End Sub
+
+' Formatea y envía un único mensaje SSE a un cliente específico.
+Sub SendMessage(out As OutputStream, eventName As String, message As String, retry As Int, id As String)
+ ' Construye el mensaje siguiendo el formato oficial del protocolo SSE.
+ Dim sb As StringBuilder
+ sb.Initialize
+ sb.Append("id: " & id).Append(CRLF)
+ sb.Append("event: " & eventName).Append(CRLF)
+ If message <> "" Then
+ sb.Append("data: " & message).Append(CRLF)
+ End If
+ If retry > 0 Then
+ sb.Append("retry: " & retry).Append(CRLF)
+ End If
+ sb.Append(CRLF) ' El doble salto de línea final es obligatorio.
+
+ ' Convierte el texto a bytes y lo escribe en el canal de comunicación del cliente.
+ Dim Bytes() As Byte = sb.ToString.GetBytes("UTF-8")
+ out.WriteBytes(Bytes, 0, Bytes.Length)
+ out.Flush ' Fuerza el envío inmediato de los datos.
+End Sub
+
+' Genera un Identificador Único Global (GUID) para cada conexión.
+Private Sub GetGUID() As String
+ Dim jo As JavaObject
+ Return jo.InitializeStatic("java.util.UUID").RunMethod("randomUUID", Null)
+End Sub
+
+
+' Evento que se dispara cada vez que el 'RemoveTimer' completa su intervalo (cada 5 segundos).
+' Su propósito es proactivamente limpiar conexiones muertas.
+Sub RemoveTimer_Tick
+' Log("remove timer")
+ ' Itera sobre todas las conexiones activas.
+ For Each key As String In Connections.Keys
+ ' Intenta enviar un mensaje de prueba ("ping" o "heartbeat") a cada cliente.
+ Try
+ ' Obtiene el OutputStream del cliente.
+ Dim out As OutputStream = Connections.Get(key)
+ ' Envía un evento de tipo "Test" sin datos. Si la conexión está viva, esto no hará nada visible.
+ SendMessage(out, "Test", "", 0, "")
+ Catch
+ ' Si el 'SendMessage' falla, significa que el socket está cerrado (el cliente se desconectó).
+ ' Registra en el log que se está eliminando una conexión muerta.
+ Log("######################")
+ Log("## Removing (timer): " & key)
+ Log("######################")
+ ' Elimina la conexión del mapa para liberar recursos.
+ Connections.Remove(key)
+ End Try
+ Next
+End Sub
+
+' Evento del Timer #2 ("El Informante"): se dispara cada 2 segundos.
+public Sub StatsTimer_Tick
+ ' Optimización: si no hay nadie conectado, no realiza el trabajo pesado.
+' Log($"Conexiones: ${Connections.Size}"$)
+ If Connections.Size = 0 Then Return
+
+ Try
+ ' Prepara un mapa para almacenar las estadísticas recolectadas.
+ Dim allPoolStats As Map
+ allPoolStats.Initialize
+
+ ' Bloquea el acceso a los conectores para leer sus datos de forma segura.
+ Main.MainConnectorsLock.RunMethod("lock", Null)
+ For Each dbKey As String In Main.listaDeCP
+ Dim connector As RDCConnector
+ If Main.Connectors.ContainsKey(dbKey) Then
+ connector = Main.Connectors.Get(dbKey)
+ If connector.IsInitialized Then
+ allPoolStats.Put(dbKey, connector.GetPoolStats)
+ Else
+ allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
+ End If
+ End If
+ Next
+ ' Libera el bloqueo para que otras partes del programa puedan usar los conectores.
+ Main.MainConnectorsLock.RunMethod("unlock", Null)
+
+ ' Convierte el mapa de estadísticas a un formato de texto JSON.
+ Dim j As JSONGenerator
+ j.Initialize(allPoolStats)
+ Dim jsonStats As String = j.ToString
+
+ ' Llama al "locutor" para enviar el JSON a todos los clientes conectados.
+ Broadcast("stats", "stats_update", jsonStats, 0)
+
+ Catch
+ ' Captura y registra cualquier error que ocurra durante la recolección de datos.
+ Log($"[SSE] Error CRÍTICO durante la adquisición de estadísticas: ${LastException.Message}"$)
+ End Try
+End Sub
\ No newline at end of file
diff --git a/jRDC_Multi.b4j b/jRDC_Multi.b4j
index e7c92e2..dff9663 100644
--- a/jRDC_Multi.b4j
+++ b/jRDC_Multi.b4j
@@ -1,17 +1,19 @@
AppType=StandardJava
Build1=Default,b4j.JRDCMulti
File1=config.DB2.properties
-File10=stop.bat
+File10=start2.bat
+File11=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
+File6=manager.html
+File7=reiniciaProcesoBow.bat
+File8=reiniciaProcesoPM2.bat
+File9=start.bat
FileGroup1=Default Group
FileGroup10=Default Group
+FileGroup11=Default Group
FileGroup2=Default Group
FileGroup3=Default Group
FileGroup4=Default Group
@@ -36,7 +38,9 @@ Module11=Manager0
Module12=ParameterValidationUtils
Module13=ping
Module14=RDCConnector
-Module15=TestHandler
+Module15=SSE
+Module16=SSEHandler
+Module17=TestHandler
Module2=ChangePassHandler
Module3=DBHandlerB4X
Module4=DBHandlerJSON
@@ -45,9 +49,9 @@ Module6=faviconHandler
Module7=GlobalParameters
Module8=LoginHandler
Module9=LogoutHandler
-NumberOfFiles=10
+NumberOfFiles=11
NumberOfLibraries=9
-NumberOfModules=15
+NumberOfModules=17
Version=10.3
@EndOfDesignText@
'Non-UI application (console / server application)
@@ -56,7 +60,7 @@ Version=10.3
#CommandLineArgs:
#MergeLibraries: True
-' VERSION 5.09.17
+' VERSION 5.09.18
'###########################################################################################################
'###################### PULL #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull
@@ -129,9 +133,13 @@ Sub Process_Globals
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
@@ -139,6 +147,8 @@ Sub AppStart (Args() As String)
logger = False
#End If
' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
+
+' SSE.Initialize
bc.Initialize("BC")
QueryLogCache.Initialize
@@ -161,6 +171,7 @@ Sub AppStart (Args() As String)
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
@@ -311,6 +322,7 @@ Sub AppStart (Args() As String)
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.
@@ -545,7 +557,7 @@ Public Sub WriteQueryLogsBatch
MainConnectorsLock.RunMethod("unlock", Null)
- If logger Then Log($"[LOG BATCH] Iniciando escritura transaccional de ${batchSize} logs de rendimiento. Logs copiados: ${logsToWrite.Size}"$)
+' If logger Then Log($"[LOG BATCH] Iniciando escritura transaccional de ${batchSize} logs de rendimiento. Logs copiados: ${logsToWrite.Size}"$)
' === PASO 2: Escritura Transaccional a SQLite ===
@@ -563,7 +575,7 @@ Public Sub WriteQueryLogsBatch
' 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."$)
+ 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.
diff --git a/jRDC_Multi.b4j.meta b/jRDC_Multi.b4j.meta
index c267407..f2cf27d 100644
--- a/jRDC_Multi.b4j.meta
+++ b/jRDC_Multi.b4j.meta
@@ -6,6 +6,8 @@ ModuleBookmarks12=
ModuleBookmarks13=
ModuleBookmarks14=
ModuleBookmarks15=
+ModuleBookmarks16=
+ModuleBookmarks17=
ModuleBookmarks2=
ModuleBookmarks3=
ModuleBookmarks4=
@@ -22,6 +24,8 @@ ModuleBreakpoints12=
ModuleBreakpoints13=
ModuleBreakpoints14=
ModuleBreakpoints15=
+ModuleBreakpoints16=
+ModuleBreakpoints17=
ModuleBreakpoints2=
ModuleBreakpoints3=
ModuleBreakpoints4=
@@ -37,7 +41,9 @@ ModuleClosedNodes11=
ModuleClosedNodes12=
ModuleClosedNodes13=
ModuleClosedNodes14=
-ModuleClosedNodes15=
+ModuleClosedNodes15=3,5,6
+ModuleClosedNodes16=2,3
+ModuleClosedNodes17=
ModuleClosedNodes2=
ModuleClosedNodes3=
ModuleClosedNodes4=
@@ -46,6 +52,6 @@ ModuleClosedNodes6=
ModuleClosedNodes7=
ModuleClosedNodes8=
ModuleClosedNodes9=
-NavigationStack=DBHandlerJSON,SendSuccessResponse,253,0,DBHandlerJSON,CleanupAndLog,248,0,RDCConnector,Class_Globals,21,0,RDCConnector,Initialize,35,0,Main,LogServerError,453,0,DBHandlerB4X,ExecuteBatch2,342,0,DBHandlerJSON,Class_Globals,7,0,DBHandlerB4X,ExecuteBatch,445,6,DBHandlerJSON,Handle,201,6,Main,borraArribaDe15000Logs,623,0,Cambios,Process_Globals,22,0
+NavigationStack=SSE,GetGUID,115,0,SSE,RemoveTimer_Tick,124,5,RDCConnector,GetPoolStats,255,0,Manager,Class_Globals,10,0,Manager,Initialize,24,0,Manager,Handle,149,0,SSEHandler,RemoveTimer_Tick,66,0,SSEHandler,Class_Globals,16,0,Main,AppStart,265,0,Cambios,Process_Globals,25,0
SelectedBuild=0
-VisibleModules=3,4,14,1,10,12
+VisibleModules=3,4,14,1,10,15,16,17,13