mirror of
https://github.com/KeymonSoft/jRDC-Multi.git
synced 2026-04-17 12:56:23 +00:00
- 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.
This commit is contained in:
12
Cambios.bas
12
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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
391
Files/www/manager.html
Normal file
391
Files/www/manager.html
Normal file
@@ -0,0 +1,391 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>jRDC2-Multi - Panel de Administración</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Helvetica, Arial, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.admin-menu {
|
||||
background-color: #e9ecef;
|
||||
padding: 1.5em 2em;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
.admin-menu h2 {
|
||||
margin: 0 0 0.2em 0;
|
||||
color: #343a40;
|
||||
}
|
||||
.admin-menu p {
|
||||
margin: 0 0 1em 0;
|
||||
color: #495057;
|
||||
}
|
||||
.admin-menu a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
margin-right: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
border-right: 1px solid #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
.admin-menu a:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
.admin-menu a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.main-content {
|
||||
padding: 2em;
|
||||
}
|
||||
#output-container {
|
||||
background: #fff;
|
||||
padding: 1em;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
min-height: 200px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1.5em;
|
||||
font-family: sans-serif;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 10px 12px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
thead {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
th {
|
||||
font-weight: 600;
|
||||
cursor: help;
|
||||
}
|
||||
tbody td:first-child {
|
||||
font-weight: bold;
|
||||
color: #0056b3;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* --- ESTILOS PARA EL INDICADOR SSE --- */
|
||||
.sse-status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 1em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.sse-status {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
.sse-connected {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
.sse-disconnected {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
.sse-connecting {
|
||||
background-color: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
.cell-update {
|
||||
animation: flash 0.7s ease-out;
|
||||
}
|
||||
@keyframes flash {
|
||||
0% {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-menu">
|
||||
<h2>Panel de Administración jRDC</h2>
|
||||
<p>Bienvenido, <strong>admin</strong></p>
|
||||
<nav id="main-nav">
|
||||
<a data-command="test">Test</a>
|
||||
<a data-command="ping">Ping</a>
|
||||
<a data-command="reload">Reload</a>
|
||||
<a data-command="slowqueries">Queries Lentas</a>
|
||||
<a data-command="getstats">Estadísticas Pool</a>
|
||||
<a data-command="rpm2">Reiniciar (pm2)</a>
|
||||
<a data-command="reviveBow">Revive Bow</a>
|
||||
</nav>
|
||||
<div class="sse-status-container">
|
||||
<span>Estado de Estadísticas en Tiempo Real:</span>
|
||||
<span id="sse-status" class="sse-status sse-disconnected"
|
||||
>Desconectado</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<h1 id="content-title">Bienvenido</h1>
|
||||
<div id="output-container">
|
||||
<p style="font-family: sans-serif">
|
||||
Selecciona una opción del menú para comenzar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const outputContainer = document.getElementById("output-container");
|
||||
const contentTitle = document.getElementById("content-title");
|
||||
const sseStatus = document.getElementById("sse-status");
|
||||
let sseConnection = null;
|
||||
|
||||
// --- CONFIGURACIÓN PARA LA TABLA DE ESTADÍSTICAS ---
|
||||
const COLUMN_ORDER = [
|
||||
"InitialPoolSize",
|
||||
"MinPoolSize",
|
||||
"MaxPoolSize",
|
||||
"AcquireIncrement",
|
||||
"TotalConnections",
|
||||
"BusyConnections",
|
||||
"IdleConnections",
|
||||
"CheckoutTimeout",
|
||||
"MaxIdleTime",
|
||||
"MaxConnectionAge",
|
||||
];
|
||||
const HEADER_TOOLTIPS = {
|
||||
InitialPoolSize:
|
||||
"Número de conexiones que el pool intenta adquirir al arrancar.",
|
||||
MinPoolSize: "Número mínimo de conexiones que el pool mantendrá.",
|
||||
MaxPoolSize: "Número máximo de conexiones que el pool puede mantener.",
|
||||
AcquireIncrement:
|
||||
"Número de conexiones a adquirir cuando el pool se queda sin conexiones.",
|
||||
TotalConnections: "Número total de conexiones (ocupadas + libres).",
|
||||
BusyConnections: "Número de conexiones activamente en uso.",
|
||||
IdleConnections: "Número de conexiones disponibles en el pool.",
|
||||
CheckoutTimeout: "Tiempo máximo de espera por una conexión (ms).",
|
||||
MaxIdleTime:
|
||||
"Tiempo máximo que una conexión puede estar inactiva (segundos).",
|
||||
MaxConnectionAge: "Tiempo máximo de vida de una conexión (segundos).",
|
||||
};
|
||||
|
||||
// --- MANEJO DE LA CONEXIÓN SSE ---
|
||||
|
||||
function connectSSE() {
|
||||
if (sseConnection && sseConnection.readyState !== EventSource.CLOSED) {
|
||||
return; // Ya está conectado o conectando
|
||||
}
|
||||
|
||||
outputContainer.innerHTML =
|
||||
'<p style="font-family: sans-serif;">Esperando datos del pool de conexiones...</p>';
|
||||
updateSSEStatus("connecting");
|
||||
|
||||
// La ruta debe coincidir con la que registraste en srvr.AddHandler
|
||||
const SSE_ENDPOINT = "/stats-stream";
|
||||
sseConnection = new EventSource(SSE_ENDPOINT);
|
||||
|
||||
sseConnection.onopen = () => {
|
||||
console.log("Conexión SSE establecida.");
|
||||
updateSSEStatus("connected");
|
||||
};
|
||||
|
||||
// Escucha el evento específico "stats_update"
|
||||
sseConnection.addEventListener("stats_update", (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
renderOrUpdateStatsTable(data);
|
||||
} catch (e) {
|
||||
console.error("Error al parsear datos SSE:", e);
|
||||
}
|
||||
});
|
||||
|
||||
sseConnection.onerror = () => {
|
||||
console.error("Error en la conexión SSE. Reintentando...");
|
||||
updateSSEStatus("disconnected");
|
||||
sseConnection.close();
|
||||
// El navegador reintentará automáticamente la conexión
|
||||
};
|
||||
}
|
||||
|
||||
function disconnectSSE() {
|
||||
if (sseConnection) {
|
||||
sseConnection.close();
|
||||
sseConnection = null;
|
||||
console.log("Conexión SSE cerrada.");
|
||||
updateSSEStatus("disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
function updateSSEStatus(status) {
|
||||
switch (status) {
|
||||
case "connected":
|
||||
sseStatus.textContent = "Conectado";
|
||||
sseStatus.className = "sse-status sse-connected";
|
||||
break;
|
||||
case "connecting":
|
||||
sseStatus.textContent = "Conectando";
|
||||
sseStatus.className = "sse-status sse-connecting";
|
||||
break;
|
||||
case "disconnected":
|
||||
sseStatus.textContent = "Desconectado";
|
||||
sseStatus.className = "sse-status sse-disconnected";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- RENDERIZADO Y ACTUALIZACIÓN DE LA TABLA ---
|
||||
|
||||
function renderOrUpdateStatsTable(data) {
|
||||
const table = document.getElementById("stats-table");
|
||||
// Si la tabla no existe, la crea
|
||||
if (!table) {
|
||||
outputContainer.innerHTML = createStatsTableHTML(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Si la tabla ya existe, solo actualiza las celdas
|
||||
for (const dbKey in data) {
|
||||
const poolData = data[dbKey];
|
||||
COLUMN_ORDER.forEach((metric) => {
|
||||
const cell = document.getElementById(`${dbKey}_${metric}`);
|
||||
if (cell && cell.textContent != poolData[metric]) {
|
||||
cell.textContent = poolData[metric];
|
||||
cell.classList.add("cell-update");
|
||||
setTimeout(() => cell.classList.remove("cell-update"), 700);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createStatsTableHTML(data) {
|
||||
let tableHtml = `<table id="stats-table"><thead><tr><th title="Nombre de la Base de Datos">DB Key</th>`;
|
||||
COLUMN_ORDER.forEach((key) => {
|
||||
tableHtml += `<th title="${HEADER_TOOLTIPS[key] || ""}">${key}</th>`;
|
||||
});
|
||||
tableHtml += `</tr></thead><tbody>`;
|
||||
for (const dbKey in data) {
|
||||
const poolData = data[dbKey];
|
||||
tableHtml += `<tr><td>${dbKey}</td>`;
|
||||
COLUMN_ORDER.forEach((metric) => {
|
||||
// Se añade un ID único a cada celda: "DB1_TotalConnections"
|
||||
tableHtml += `<td id="${dbKey}_${metric}">${
|
||||
poolData[metric] ?? "N/A"
|
||||
}</td>`;
|
||||
});
|
||||
tableHtml += `</tr>`;
|
||||
}
|
||||
tableHtml += `</tbody></table>`;
|
||||
return tableHtml;
|
||||
}
|
||||
|
||||
// --- MANEJO DE COMANDOS ESTÁTICOS (SIN CAMBIOS) ---
|
||||
|
||||
async function loadStaticContent(command) {
|
||||
contentTitle.textContent = `Resultado del Comando: '${command}'`;
|
||||
outputContainer.innerHTML = "Cargando...";
|
||||
try {
|
||||
const response = await fetch(`/manager?command=${command}`);
|
||||
const responseText = await response.text();
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`Error del servidor (${response.status}): ${responseText}`
|
||||
);
|
||||
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (contentType && contentType.includes("application/json")) {
|
||||
const data = JSON.parse(responseText);
|
||||
if (command === "slowqueries") {
|
||||
outputContainer.innerHTML = data.message
|
||||
? `<p>${data.message}</p>`
|
||||
: createTableFromJSON(data.data);
|
||||
} else {
|
||||
outputContainer.textContent = JSON.stringify(data, null, 2);
|
||||
}
|
||||
} else {
|
||||
outputContainer.textContent = responseText;
|
||||
}
|
||||
} catch (error) {
|
||||
outputContainer.textContent = `Error al procesar la respuesta:\n${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function createTableFromJSON(data) {
|
||||
if (!data || data.length === 0)
|
||||
return "<p>No se encontraron queries lentas.</p>";
|
||||
const headers = Object.keys(data[0]);
|
||||
let table = "<table><thead><tr>";
|
||||
headers.forEach((h) => (table += `<th>${h.replace(/_/g, " ")}</th>`));
|
||||
table += "</tr></thead><tbody>";
|
||||
data.forEach((row) => {
|
||||
table += "<tr>";
|
||||
headers.forEach((h) => (table += `<td>${row[h]}</td>`));
|
||||
table += "</tr>";
|
||||
});
|
||||
table += "</tbody></table>";
|
||||
return table;
|
||||
}
|
||||
|
||||
// --- EVENT LISTENER PRINCIPAL ---
|
||||
|
||||
document.getElementById("main-nav").addEventListener("click", (event) => {
|
||||
if (event.target.tagName === "A") {
|
||||
const command = event.target.dataset.command;
|
||||
if (!command) return;
|
||||
|
||||
if (command === "reload") {
|
||||
// Pedimos al usuario la DB Key. Si la deja vacía, se asume recarga total.
|
||||
const dbKey = prompt(
|
||||
"Ingrese la llave de la DB a recargar (ej: DB2, DB3). Deje vacío para recargar TODAS:",
|
||||
""
|
||||
);
|
||||
if (dbKey === null) {
|
||||
// El usuario presionó Cancelar o cerró la ventana. NO HACER NADA.
|
||||
outputContainer.textContent = "Recarga cancelada por el usuario.";
|
||||
contentTitle.textContent = "Administración";
|
||||
return;
|
||||
}
|
||||
let finalCommand = "reload";
|
||||
if (dbKey && dbKey.trim() !== "") {
|
||||
// Si el usuario especificó una DB (ej. DB2), construimos el comando con el parámetro 'db'.
|
||||
const key = dbKey.toUpperCase().trim();
|
||||
finalCommand = `reload&db=${key}`;
|
||||
outputContainer.innerHTML = `<p style="font-family: sans-serif;">Intentando recargar: <b>${key}</b> (Hot-Swap)...</p>`;
|
||||
} else {
|
||||
// Recarga total.
|
||||
outputContainer.innerHTML = `<p style="font-family: sans-serif;">Intentando recargar: <b>TODAS</b> (Hot-Swap)...</p>`;
|
||||
}
|
||||
disconnectSSE();
|
||||
// Llamamos a la función loadStaticContent con el comando completo (ej: 'reload&db=DB2' o 'reload')
|
||||
loadStaticContent(finalCommand);
|
||||
} else if (command === "getstats") {
|
||||
contentTitle.textContent = `Estadísticas del Pool en Tiempo Real`;
|
||||
connectSSE();
|
||||
} else {
|
||||
// Si se selecciona cualquier otro comando, se desconecta del SSE
|
||||
disconnectSSE();
|
||||
loadStaticContent(command);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
389
Manager.bas
389
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
|
||||
@@ -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,
|
||||
|
||||
187
SSE.bas
Normal file
187
SSE.bas
Normal file
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user