perf(dashboard): paraleliza la carga de métricas SQL Server por nodo (#11)
Some checks failed
Aduanasoft/PANEL_BASES_ANEXO24/pipeline/head There was a failure building this commit
Some checks failed
Aduanasoft/PANEL_BASES_ANEXO24/pipeline/head There was a failure building this commit
La carga inicial del dashboard (/+page.server.ts) tardaba ~30s porque loadSqlDashboardFromNodes recorría los nodos en serie y hacía 3 round-trips secuenciales por nodo (métricas, alerta, historial) → N×3 viajes encadenados a SQL Server remoto. Cambios: - Las 3 consultas por nodo ahora corren con Promise.all. - Los nodos se procesan con concurrencia acotada (8) vía mapWithConcurrency; la agregación se mantiene secuencial para conservar orden y evitar carreras. - getMssqlPoolMaster cachea la *promesa* del pool y reserva el slot de forma síncrona antes de cualquier await, evitando pools duplicados al paralelizar. - En +page.server.ts: métricas SQL Server y catálogo PostgreSQL se cargan en paralelo (Promise.allSettled); las 6 consultas de catálogo en Promise.all. - Se elimina la consulta listDatabaseNodes() duplicada (se reusa la ya cargada). - Hidratación de alertas y filtro de permisos paralelizados. - Se quita un console.log de depuración por fila (estándar: sin logs sueltos). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Reviewed-on: #11 Co-authored-by: AlexeerCT <acazares@aduanasoft.com.mx> Co-committed-by: AlexeerCT <acazares@aduanasoft.com.mx>
This commit is contained in:
@@ -6,7 +6,10 @@ import sql from 'mssql';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const MAX_POOLS = 16;
|
||||
const poolMap = new Map<string, sql.ConnectionPool>();
|
||||
// Se cachea la *promesa* del pool (no el pool ya resuelto) para que varias cargas de nodos
|
||||
// concurrentes sobre el mismo servidor reutilicen una sola conexión en vuelo y no abran pools
|
||||
// duplicados (condición de carrera que aparece al paralelizar el dashboard).
|
||||
const poolMap = new Map<string, Promise<sql.ConnectionPool>>();
|
||||
|
||||
export function adjustMssqlServerForDocker(serverName: string): string {
|
||||
const docker =
|
||||
@@ -52,13 +55,35 @@ async function evictPoolIfNeeded(): Promise<void> {
|
||||
const old = poolMap.get(first);
|
||||
poolMap.delete(first);
|
||||
try {
|
||||
await old?.close();
|
||||
(await old)?.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta `fn` sobre cada elemento con un máximo de `limit` tareas simultáneas.
|
||||
* Conserva el orden de `items` en el arreglo de resultados.
|
||||
*/
|
||||
async function mapWithConcurrency<T, R>(
|
||||
items: T[],
|
||||
limit: number,
|
||||
fn: (item: T, index: number) => Promise<R>
|
||||
): Promise<R[]> {
|
||||
const results: R[] = new Array(items.length);
|
||||
let cursor = 0;
|
||||
const workerCount = Math.min(Math.max(1, limit), items.length || 1);
|
||||
async function worker(): Promise<void> {
|
||||
while (cursor < items.length) {
|
||||
const index = cursor++;
|
||||
results[index] = await fn(items[index], index);
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pool conectado a `master` en el servidor del nodo (permite consultar cualquier BD con nombre de tres partes).
|
||||
*/
|
||||
@@ -71,10 +96,17 @@ export async function getMssqlPoolMaster(serverHost: string, password: string):
|
||||
}
|
||||
const server = adjustMssqlServerForDocker(serverHost);
|
||||
const key = poolCacheKey(server, user, password);
|
||||
const existing = poolMap.get(key);
|
||||
if (existing?.connected) return existing;
|
||||
|
||||
await evictPoolIfNeeded();
|
||||
const existing = poolMap.get(key);
|
||||
if (existing) {
|
||||
try {
|
||||
const pool = await existing;
|
||||
if (pool.connected || pool.connecting) return pool;
|
||||
} catch {
|
||||
/* pool inservible: se descarta y se recrea abajo */
|
||||
}
|
||||
poolMap.delete(key);
|
||||
}
|
||||
|
||||
const cfg: sql.config = {
|
||||
user,
|
||||
@@ -87,9 +119,22 @@ export async function getMssqlPoolMaster(serverHost: string, password: string):
|
||||
connectTimeout: 30000
|
||||
}
|
||||
};
|
||||
const pool = await new sql.ConnectionPool(cfg).connect();
|
||||
poolMap.set(key, pool);
|
||||
return pool;
|
||||
|
||||
// El slot se reserva en el mapa de forma SÍNCRONA (antes de cualquier await) para que las
|
||||
// cargas concurrentes al mismo servidor compartan esta conexión en vuelo y no creen pools
|
||||
// duplicados. La purga (evictPoolIfNeeded) ocurre dentro de la promesa, no antes de registrarla.
|
||||
const connecting = (async () => {
|
||||
await evictPoolIfNeeded();
|
||||
return new sql.ConnectionPool(cfg).connect();
|
||||
})();
|
||||
poolMap.set(key, connecting);
|
||||
try {
|
||||
return await connecting;
|
||||
} catch (e) {
|
||||
// No conservar en caché una conexión que falló al establecerse.
|
||||
poolMap.delete(key);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export type CatalogNodeRow = {
|
||||
@@ -236,44 +281,59 @@ export async function loadSqlDashboardFromNodes(nodes: CatalogNodeRow[]): Promis
|
||||
const restoreHistory: Record<string, { restore_date: Date }[]> = {};
|
||||
let totalSizeGb = 0;
|
||||
|
||||
for (const node of nodes) {
|
||||
// Para cada nodo las 3 consultas (métricas, alerta, historial) son independientes y se lanzan
|
||||
// en paralelo. Los nodos se procesan con concurrencia acotada para no saturar los pools.
|
||||
// Antes el patrón era N×3 round-trips en serie contra SQL Server (decenas de segundos).
|
||||
const NODE_CONCURRENCY = 8;
|
||||
const perNode = await mapWithConcurrency(nodes, NODE_CONCURRENCY, async (node) => {
|
||||
const dbn = String(node.BDName || '').trim();
|
||||
if (!dbn) continue;
|
||||
if (!dbn) return null;
|
||||
const pwd = resolveNodeSqlPassword(node.sql_password);
|
||||
try {
|
||||
const pool = await getMssqlPoolMaster(String(node.ServerName || '').trim(), pwd);
|
||||
const row = await queryDatabaseMetricsOnServer(pool, dbn);
|
||||
if (!row) continue;
|
||||
|
||||
const visible = String(row.visible_name ?? dbn);
|
||||
const keyLower = visible.toLowerCase();
|
||||
|
||||
databaseRows.push({
|
||||
...row,
|
||||
visible_name: visible,
|
||||
original_name: row.original_name ?? visible,
|
||||
NodoSubNodo: node.NodoSubNodo,
|
||||
client_name: node.Nombre,
|
||||
BDName: dbn,
|
||||
_node_id: node.ID,
|
||||
_server: adjustMssqlServerForDocker(String(node.ServerName || '').trim())
|
||||
});
|
||||
|
||||
totalSizeGb += Number(row.total_size_gb) || 0;
|
||||
|
||||
const alertRow = await queryDatabaseAlertRow(pool, dbn);
|
||||
if (alertRow) alertsData.push(alertRow);
|
||||
|
||||
const hist = await queryRestoreHistoryForDatabase(pool, dbn);
|
||||
if (!restoreHistory[keyLower]) restoreHistory[keyLower] = [];
|
||||
for (const h of hist) {
|
||||
restoreHistory[keyLower].push(h);
|
||||
}
|
||||
const [row, alertRow, hist] = await Promise.all([
|
||||
queryDatabaseMetricsOnServer(pool, dbn),
|
||||
queryDatabaseAlertRow(pool, dbn),
|
||||
queryRestoreHistoryForDatabase(pool, dbn)
|
||||
]);
|
||||
if (!row) return null;
|
||||
return { node, dbn, row, alertRow, hist };
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`SQL Server nodo id=${node.ID} server=${node.ServerName} db=${dbn}:`,
|
||||
e
|
||||
);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Agregación secuencial sobre resultados ya resueltos: evita condiciones de carrera sobre
|
||||
// las estructuras compartidas y conserva el orden original de los nodos.
|
||||
for (const res of perNode) {
|
||||
if (!res) continue;
|
||||
const { node, dbn, row, alertRow, hist } = res;
|
||||
|
||||
const visible = String(row.visible_name ?? dbn);
|
||||
const keyLower = visible.toLowerCase();
|
||||
|
||||
databaseRows.push({
|
||||
...row,
|
||||
visible_name: visible,
|
||||
original_name: row.original_name ?? visible,
|
||||
NodoSubNodo: node.NodoSubNodo,
|
||||
client_name: node.Nombre,
|
||||
BDName: dbn,
|
||||
_node_id: node.ID,
|
||||
_server: adjustMssqlServerForDocker(String(node.ServerName || '').trim())
|
||||
});
|
||||
|
||||
totalSizeGb += Number(row.total_size_gb) || 0;
|
||||
|
||||
if (alertRow) alertsData.push(alertRow);
|
||||
|
||||
if (!restoreHistory[keyLower]) restoreHistory[keyLower] = [];
|
||||
for (const h of hist) {
|
||||
restoreHistory[keyLower].push(h);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user