1 Commits

Author SHA1 Message Date
616013f0fb - 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.
2025-09-27 14:14:38 -06:00
9 changed files with 923 additions and 210 deletions

View File

@@ -21,15 +21,17 @@ Sub Process_Globals
' los dominios permitidos. ' los dominios permitidos.
' - Ej: token:1224abcd5678fghi, dominio:"keymon.net" ' - Ej: token:1224abcd5678fghi, dominio:"keymon.net"
' - Ej: token:4321abcd8765fghi, dominio:"*" ' - 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 ' - 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. ' 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 ' - VERSION 5.09.17
' - 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. ' - 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: ' - Cambios Principales:

View File

@@ -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! 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 ' Este bloque captura el número de conexiones actualmente ocupadas en el pool
' *después* de que esta petición ha obtenido la suya. ' *después* de que esta petición ha obtenido la suya.
If Connector.IsInitialized Then If Connector.IsInitialized Then
Dim poolStats As Map = Connector.GetPoolStats Dim poolStats As Map = Connector.GetPoolStats
If poolStats.ContainsKey("BusyConnections") Then If poolStats.ContainsKey("BusyConnections") Then
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>>
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor. poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor.
Log($">>>>>>>>>> ${poolStats.Get("BusyConnections")} "$)
End If End If
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. ' 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 End If
Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL --- Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL ---
Log(LastException) ' Registra la excepción completa en el log. Dim errorMessage As String = LastException.Message
Main.LogServerError("ERROR", "DBHandlerB4X.Handle", LastException.Message, dbKey, q, req.RemoteAddress) ' <-- Nuevo Log If errorMessage.Contains("ORA-01002") Or errorMessage.Contains("recuperación fuera de secuencia") Then
SendPlainTextError(resp, 500, LastException.Message) ' Envía un error 500 al cliente. 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. q = "error_in_b4x_handler" ' Aseguramos un valor para 'q' en caso de excepción.
End Try ' --- FIN: Bloque Try principal --- 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)) Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in))
' Obtiene la lista de objetos DBCommand. ' Obtiene la lista de objetos DBCommand.
Dim commands As List = m.Get("commands") 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). ' Prepara un objeto DBResult para la respuesta (aunque para batch no devuelve datos, solo confirmación).
Dim res As DBResult Dim res As DBResult
res.Initialize res.Initialize
res.columns = CreateMap("AffectedRows (N/A)": 0) ' Columna simbólica. res.columns = CreateMap("AffectedRows": 0) ' Columna simbólica.
res.Rows.Initialize res.Rows.Initialize
res.Tag = Null res.Tag = Null
@@ -390,10 +415,14 @@ Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As S
End If End If
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta el comando con la lista de parámetros validada. 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 >>> ' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH >>>
Next 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. con.TransactionSuccessful ' Si todos los comandos se ejecutaron sin error, confirma la transacción.
Catch Catch
' Si cualquier comando falla, se captura el error. ' Si cualquier comando falla, se captura el error.
@@ -427,7 +456,7 @@ End Sub
' Ejecuta un lote de comandos usando el protocolo V1. ' 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 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. ' Lee y descarta la versión del cliente.
Dim clientVersion As Float = ReadObject(in) 'ignore Dim clientVersion As Float = ReadObject(in) 'ignore
' Lee cuántos comandos vienen en el lote. ' 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 Try
con.BeginTransaction con.BeginTransaction
' Itera para procesar cada comando del lote. ' Itera para procesar cada comando del lote.
Log(numberOfStatements) ' Log(numberOfStatements)
For i = 0 To numberOfStatements - 1 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. ' Lee el nombre del comando y la lista de parámetros usando el deserializador V1.
Dim queryName As String = ReadObject(in) Dim queryName As String = ReadObject(in)
Dim params As List = ReadList(in) Dim params As List = ReadList(in)
Log(params) ' Log(params)
If numberOfStatements = 1 Then If numberOfStatements = 1 Then
singleQueryName = queryName 'Capturamos el nombre del query. singleQueryName = queryName 'Capturamos el nombre del query.
End If End If
Dim sqlCommand As String = Connector.GetCommand(DB, queryName) Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
Log(sqlCommand) ' Log(sqlCommand)
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>>
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
con.Rollback ' Deshace la transacción si un comando es inválido. 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. Return "error" ' Salida temprana si la validación falla.
End If 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. 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. 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. Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime la salida antes de enviarla.
' Escribe la respuesta usando el serializador V1. ' Escribe la respuesta usando el serializador V1.
WriteObject(Main.VERSION, out) WriteObject(Main.VERSION, out)
WriteObject("batch", out) WriteObject("batch", out)
WriteInt(res.Length, out) WriteInt(res.Length, out)
Log(affectedCounts.Size) ' Log(affectedCounts.Size)
For Each r As Int In affectedCounts For Each r As Int In affectedCounts
WriteInt(r, out) WriteInt(r, out)
Next Next

View File

@@ -128,10 +128,30 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
If poolStats.ContainsKey("BusyConnections") Then If poolStats.ContainsKey("BusyConnections") Then
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>> ' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>>
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor. poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor.
' Log($">>>>>>>>>> ${poolStats.Get("BusyConnections")} "$)
End If End If
End If End If
' <<<< ¡FIN DE CAPTURA! >>>> ' <<<< ¡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. ' Obtiene la sentencia SQL correspondiente al nombre del comando desde config.properties.
Dim sqlCommand As String = Connector.GetCommand(finalDbKey, queryNameForLog) Dim sqlCommand As String = Connector.GetCommand(finalDbKey, queryNameForLog)

391
Files/www/manager.html Normal file
View 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>

View File

@@ -30,7 +30,8 @@ End Sub
' Método principal que maneja las peticiones HTTP para el panel de administración. ' 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. ' Refactorizado para funcionar como una API con un frontend estático.
Sub Handle(req As ServletRequest, resp As ServletResponse) 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 If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
resp.SendRedirect("/login") resp.SendRedirect("/login")
Return Return
@@ -39,7 +40,6 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Dim Command As String = req.GetParameter("command") Dim Command As String = req.GetParameter("command")
' --- 2. Servidor de la Página Principal --- ' --- 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 If Command = "" Then
Try Try
resp.ContentType = "text/html; charset=utf-8" resp.ContentType = "text/html; charset=utf-8"
@@ -51,16 +51,13 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
End If End If
' --- 3. Manejo de Comandos como API --- ' --- 3. Manejo de Comandos como API ---
' La variable 'j' (JSONGenerator) está en Class_Globals
Select Command.ToLowerCase Select Command.ToLowerCase
' --- Comandos que devuelven JSON --- ' --- Comandos que devuelven JSON (Métricas del Pool) ---
Case "getstats" Case "getstatsold"
resp.ContentType = "application/json; charset=utf-8" resp.ContentType = "application/json; charset=utf-8"
Dim allPoolStats As Map Dim allPoolStats As Map
allPoolStats.Initialize allPoolStats.Initialize
For Each dbKey As String In Main.listaDeCP For Each dbKey As String In Main.listaDeCP
Dim connector As RDCConnector = Main.Connectors.Get(dbKey) Dim connector As RDCConnector = Main.Connectors.Get(dbKey)
If connector.IsInitialized Then If connector.IsInitialized Then
@@ -69,6 +66,22 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado")) allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
End If End If
Next 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) j.Initialize(allPoolStats)
resp.Write(j.ToString) resp.Write(j.ToString)
@@ -78,17 +91,18 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
resp.ContentType = "application/json; charset=utf-8" resp.ContentType = "application/json; charset=utf-8"
Dim results As List Dim results As List
results.Initialize results.Initialize
Try 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 Dim tableExists As Boolean = Main.SQL1.ExecQuerySingleResult($"SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs';"$) <> Null
If tableExists = False Then 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.")) j.Initialize(CreateMap("message": "La tabla de logs ('query_logs') no existe. Habilita 'enableSQLiteLogs=1' en la configuración."))
resp.Write(j.ToString) resp.Write(j.ToString)
Return Return
End If 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 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"$) 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,28 +120,19 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Loop Loop
rs.Close rs.Close
' 1. Creamos un mapa "raíz" para contener nuestra lista.
Dim root As Map Dim root As Map
root.Initialize root.Initialize
root.Put("data", results) ' La llave puede ser lo que quieras, "data" es común. root.Put("data", results)
' 2. Ahora sí, inicializamos el generador con el mapa raíz.
j.Initialize(root) j.Initialize(root)
resp.Write(j.ToString) resp.Write(j.ToString)
Catch Catch
Log("Error CRÍTICO al obtener queries lentas en Manager API: " & LastException.Message) Log("Error CRÍTICO al obtener queries lentas en Manager API: " & LastException.Message)
resp.Status = 500
' <<< 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.
Dim root As Map Dim root As Map
root.Initialize root.Initialize
root.Put("data", results) ' La llave puede ser lo que quieras, "data" es común. root.Put("data", results)
' 2. Ahora sí, inicializamos el generador con el mapa raíz.
j.Initialize(root) j.Initialize(root)
resp.Write(j.ToString) resp.Write(j.ToString)
End Try End Try
@@ -156,159 +161,207 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Case "reload" Case "reload"
resp.ContentType = "text/plain; charset=utf-8" resp.ContentType = "text/plain; charset=utf-8"
Dim sbTemp As StringBuilder Dim sbTemp As StringBuilder
sbTemp.Initialize sbTemp.Initialize
' <<< LÓGICA ORIGINAL: Se mantiene intacta toda la lógica de recarga >>> ' ***** LÓGICA DE RECARGA GRANULAR/SELECTIVA *****
' (Copiada y pegada directamente de tu código anterior) Dim dbKeyToReload As String = req.GetParameter("db").ToUpperCase ' Leer parámetro 'db' opcional (ej: /manager?command=reload&db=DB3)
sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF) Dim targets As List ' Lista de DBKeys a recargar.
Dim oldTimerState As Boolean = Main.timerLogs.Enabled targets.Initialize
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)
For Each dbKey As String In Main.listaDeCP ' 1. Determinar el alcance de la recarga (selectiva o total)
Try If dbKeyToReload.Length > 0 Then
Dim newRDC As RDCConnector ' Recarga selectiva
newRDC.Initialize(dbKey) If Main.listaDeCP.IndexOf(dbKeyToReload) = -1 Then
Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0) resp.Write($"ERROR: DBKey '${dbKeyToReload}' no es válida o no está configurada."$)
Dim isEnabled As Boolean = (enableLogsSetting = 1) Return
newConnectors.Put(dbKey & "_LOG_STATE", isEnabled) End If
sbTemp.Append($" -> Logs de ${dbKey} activados: ${isEnabled}"$).Append(" " & CRLF) targets.Add(dbKeyToReload)
newConnectors.Put(dbKey, newRDC) sbTemp.Append($"Iniciando recarga selectiva de ${dbKeyToReload} (Hot-Swap)..."$).Append(" " & CRLF)
Dim newPoolStats As Map = newRDC.GetPoolStats Else
sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append(" " & CRLF) ' Recarga completa (comportamiento por defecto)
Catch targets.AddAll(Main.listaDeCP)
sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF) sbTemp.Append($"Iniciando recarga COMPLETA de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF)
reloadSuccessful = False End If
Exit
End Try
Next
If reloadSuccessful Then ' 2. Deshabilitar el Timer de logs (si es necesario)
Main.MainConnectorsLock.RunMethod("lock", Null) Dim oldTimerState As Boolean = Main.timerLogs.Enabled
Main.Connectors = newConnectors If oldTimerState Then
Main.MainConnectorsLock.RunMethod("unlock", Null) Main.timerLogs.Enabled = False
Main.SQLiteLoggingStatusByDB.Clear sbTemp.Append(" -> Timer de limpieza de logs (SQLite) detenido temporalmente.").Append(" " & CRLF)
Dim isAnyEnabled As Boolean = False End If
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
' <<< CAMBIO: Se devuelve el contenido del StringBuilder como texto plano >>> Dim reloadSuccessful As Boolean = True
resp.Write(sbTemp.ToString) Dim oldConnectorsToClose As Map ' Guardaremos los conectores antiguos aquí.
Return oldConnectorsToClose.Initialize
Case "test" ' 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
' 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" resp.ContentType = "text/plain; charset=utf-8"
Dim sb As StringBuilder Dim sb As StringBuilder
sb.Initialize 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) sb.Append("Connection successful." & CRLF & CRLF)
Dim estaDB As String = "" Dim estaDB As String = ""
Log(Main.listaDeCP) Log(Main.listaDeCP)
For i = 0 To Main.listaDeCP.Size - 1 For i = 0 To Main.listaDeCP.Size - 1
If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i) If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i)
sb.Append($"Using config${estaDB}.properties"$ & CRLF) sb.Append($"Using config${estaDB}.properties"$ & CRLF)
Next Next
con.Close con.Close
resp.Write(sb.ToString) resp.Write(sb.ToString)
Catch Catch
resp.Write("Error fetching connection: " & LastException.Message) resp.Write("Error fetching connection: " & LastException.Message)
End Try End Try
Return Return
Case "rsx", "rpm2", "revivebow", "restartserver" Case "rsx", "rpm2", "revivebow", "restartserver"
resp.ContentType = "text/plain; charset=utf-8" resp.ContentType = "text/plain; charset=utf-8"
Dim batFile As String Dim batFile As String
Select Command Select Command
Case "rsx": batFile = "start.bat" Case "rsx": batFile = "start.bat"
Case "rpm2": batFile = "reiniciaProcesoPM2.bat" Case "rpm2": batFile = "reiniciaProcesoPM2.bat"
Case "reviveBow": batFile = "reiniciaProcesoBow.bat" Case "reviveBow": batFile = "reiniciaProcesoBow.bat"
Case "restartserver": batFile = "restarServer.bat" Case "restartserver": batFile = "restarServer.bat" ' Nota: este bat no estaba definido, se usó el nombre del comando
End Select 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
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
Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) Case "paused", "continue"
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)
Case "paused", "continue"
resp.ContentType = "text/plain; charset=utf-8" resp.ContentType = "text/plain; charset=utf-8"
If Command = "paused" Then If Command = "paused" Then
GlobalParameters.IsPaused = 1 GlobalParameters.IsPaused = 1
resp.Write("Servidor pausado.") resp.Write("Servidor pausado.")
Else Else
GlobalParameters.IsPaused = 0 GlobalParameters.IsPaused = 0
resp.Write("Servidor reanudado.") resp.Write("Servidor reanudado.")
End If End If
Return Return
Case "block", "unblock" Case "block", "unblock"
resp.ContentType = "text/plain; charset=utf-8" resp.ContentType = "text/plain; charset=utf-8"
Dim ip As String = req.GetParameter("IP") 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
Case Else 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
Case Else
resp.ContentType = "text/plain; charset=utf-8" resp.ContentType = "text/plain; charset=utf-8"
resp.SendError(404, $"Comando desconocido: '{Command}'"$) resp.SendError(404, $"Comando desconocido: '{Command}'"$)
Return Return
End Select
End Select
End Sub End Sub

View File

@@ -78,15 +78,18 @@ Public Sub Initialize(DB As String)
Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2) Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2)
Dim maxPoolSize As Int = config.GetDefault("MaxPoolSize", 5) Dim maxPoolSize As Int = config.GetDefault("MaxPoolSize", 5)
Dim acquireIncrement As Int = config.GetDefault("AcquireIncrement", 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: ' 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("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("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("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("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("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(config.GetDefault("MaxConnectionAge", 900))) ' Tiempo máximo de vida de una conexión (segundos). 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(config.GetDefault("CheckoutTimeout", 60000))) ' Tiempo máximo de espera por una conexión del pool (milisegundos). 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: ' 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. ' 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. tempCon.Close ' Devolvemos la conexión inmediatamente al pool para que esté disponible.
End If 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 [ ' com.mchange.v2.c3p0.ComboPooledDataSource [
' acquireIncrement -> 3, ' acquireIncrement -> 3,
' acquireRetryAttempts -> 30, ' acquireRetryAttempts -> 30,

187
SSE.bas Normal file
View 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

View File

@@ -1,17 +1,19 @@
AppType=StandardJava AppType=StandardJava
Build1=Default,b4j.JRDCMulti Build1=Default,b4j.JRDCMulti
File1=config.DB2.properties File1=config.DB2.properties
File10=stop.bat File10=start2.bat
File11=stop.bat
File2=config.DB3.properties File2=config.DB3.properties
File3=config.DB4.properties File3=config.DB4.properties
File4=config.properties File4=config.properties
File5=login.html File5=login.html
File6=reiniciaProcesoBow.bat File6=manager.html
File7=reiniciaProcesoPM2.bat File7=reiniciaProcesoBow.bat
File8=start.bat File8=reiniciaProcesoPM2.bat
File9=start2.bat File9=start.bat
FileGroup1=Default Group FileGroup1=Default Group
FileGroup10=Default Group FileGroup10=Default Group
FileGroup11=Default Group
FileGroup2=Default Group FileGroup2=Default Group
FileGroup3=Default Group FileGroup3=Default Group
FileGroup4=Default Group FileGroup4=Default Group
@@ -36,7 +38,9 @@ Module11=Manager0
Module12=ParameterValidationUtils Module12=ParameterValidationUtils
Module13=ping Module13=ping
Module14=RDCConnector Module14=RDCConnector
Module15=TestHandler Module15=SSE
Module16=SSEHandler
Module17=TestHandler
Module2=ChangePassHandler Module2=ChangePassHandler
Module3=DBHandlerB4X Module3=DBHandlerB4X
Module4=DBHandlerJSON Module4=DBHandlerJSON
@@ -45,9 +49,9 @@ Module6=faviconHandler
Module7=GlobalParameters Module7=GlobalParameters
Module8=LoginHandler Module8=LoginHandler
Module9=LogoutHandler Module9=LogoutHandler
NumberOfFiles=10 NumberOfFiles=11
NumberOfLibraries=9 NumberOfLibraries=9
NumberOfModules=15 NumberOfModules=17
Version=10.3 Version=10.3
@EndOfDesignText@ @EndOfDesignText@
'Non-UI application (console / server application) 'Non-UI application (console / server application)
@@ -56,7 +60,7 @@ Version=10.3
#CommandLineArgs: #CommandLineArgs:
#MergeLibraries: True #MergeLibraries: True
' VERSION 5.09.17 ' VERSION 5.09.18
'########################################################################################################### '###########################################################################################################
'###################### PULL ############################################################# '###################### PULL #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=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 Public LOG_CACHE_THRESHOLD As Int = 350 ' Umbral de registros para forzar la escritura
Dim logger As Boolean Dim logger As Boolean
Public LatestPoolStats As Map ' Mapa Thread-Safe para almacenar las últimas métricas de cada pool.
End Sub End Sub
Sub AppStart (Args() As String) Sub AppStart (Args() As String)
SSE.Initialize
#if DEBUG #if DEBUG
logger = True logger = True
LOG_CACHE_THRESHOLD = 10 LOG_CACHE_THRESHOLD = 10
@@ -140,6 +148,8 @@ Sub AppStart (Args() As String)
#End If #End If
' --- Subrutina principal que se ejecuta al iniciar la aplicación --- ' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
' SSE.Initialize
bc.Initialize("BC") bc.Initialize("BC")
QueryLogCache.Initialize QueryLogCache.Initialize
ErrorLogCache.Initialize ErrorLogCache.Initialize
@@ -161,6 +171,7 @@ Sub AppStart (Args() As String)
srvr.Initialize("") srvr.Initialize("")
Connectors = srvr.CreateThreadSafeMap Connectors = srvr.CreateThreadSafeMap
commandsMap.Initialize commandsMap.Initialize
LatestPoolStats = srvr.CreateThreadSafeMap ' Inicializar el mapa de estadísticas como Thread-Safe
' NUEVO: Inicializar el mapa de estado de logs granular ' NUEVO: Inicializar el mapa de estado de logs granular
SQLiteLoggingStatusByDB.Initialize SQLiteLoggingStatusByDB.Initialize
@@ -311,6 +322,7 @@ Sub AppStart (Args() As String)
srvr.AddHandler("/DBJ", "DBHandlerJSON", False) srvr.AddHandler("/DBJ", "DBHandlerJSON", False)
srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) srvr.AddHandler("/dbrquery", "DBHandlerJSON", False)
srvr.AddHandler("/favicon.ico", "faviconHandler", False) srvr.AddHandler("/favicon.ico", "faviconHandler", False)
srvr.AddHandler("/stats-stream", "SSEHandler", False)
srvr.AddHandler("/*", "DBHandlerB4X", False) srvr.AddHandler("/*", "DBHandlerB4X", False)
' 7. Inicia el servidor HTTP. ' 7. Inicia el servidor HTTP.
@@ -545,7 +557,7 @@ Public Sub WriteQueryLogsBatch
MainConnectorsLock.RunMethod("unlock", Null) 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 === ' === PASO 2: Escritura Transaccional a SQLite ===
@@ -563,7 +575,7 @@ Public Sub WriteQueryLogsBatch
' 2. Finalizar la transacción: Escritura eficiente a disco. ' 2. Finalizar la transacción: Escritura eficiente a disco.
SQL1.TransactionSuccessful 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 Catch
' Si falla, deshacemos todos los logs del lote y registramos el fallo. ' Si falla, deshacemos todos los logs del lote y registramos el fallo.

View File

@@ -6,6 +6,8 @@ ModuleBookmarks12=
ModuleBookmarks13= ModuleBookmarks13=
ModuleBookmarks14= ModuleBookmarks14=
ModuleBookmarks15= ModuleBookmarks15=
ModuleBookmarks16=
ModuleBookmarks17=
ModuleBookmarks2= ModuleBookmarks2=
ModuleBookmarks3= ModuleBookmarks3=
ModuleBookmarks4= ModuleBookmarks4=
@@ -22,6 +24,8 @@ ModuleBreakpoints12=
ModuleBreakpoints13= ModuleBreakpoints13=
ModuleBreakpoints14= ModuleBreakpoints14=
ModuleBreakpoints15= ModuleBreakpoints15=
ModuleBreakpoints16=
ModuleBreakpoints17=
ModuleBreakpoints2= ModuleBreakpoints2=
ModuleBreakpoints3= ModuleBreakpoints3=
ModuleBreakpoints4= ModuleBreakpoints4=
@@ -37,7 +41,9 @@ ModuleClosedNodes11=
ModuleClosedNodes12= ModuleClosedNodes12=
ModuleClosedNodes13= ModuleClosedNodes13=
ModuleClosedNodes14= ModuleClosedNodes14=
ModuleClosedNodes15= ModuleClosedNodes15=3,5,6
ModuleClosedNodes16=2,3
ModuleClosedNodes17=
ModuleClosedNodes2= ModuleClosedNodes2=
ModuleClosedNodes3= ModuleClosedNodes3=
ModuleClosedNodes4= ModuleClosedNodes4=
@@ -46,6 +52,6 @@ ModuleClosedNodes6=
ModuleClosedNodes7= ModuleClosedNodes7=
ModuleClosedNodes8= ModuleClosedNodes8=
ModuleClosedNodes9= 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 SelectedBuild=0
VisibleModules=3,4,14,1,10,12 VisibleModules=3,4,14,1,10,15,16,17,13