kukuy 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ctagsd/ctagsd.json +954 -0
- package/.ctagsd/file_list.txt +100 -0
- package/.ctagsd/tags.db +0 -0
- package/CHANGELOG.md +101 -0
- package/LICENSE +680 -0
- package/README.md +251 -0
- package/captura.png +0 -0
- package/kukuy.js +23 -0
- package/kukuy.workspace +11 -0
- package/package.json +26 -0
- package/restart-balancer.sh +10 -0
- package/routes.json +14 -0
- package/scripts/load_test.py +151 -0
- package/servers.json +19 -0
- package/servers_real.json +19 -0
- package/src/algorithms/AlgorithmManager.js +85 -0
- package/src/algorithms/IPHashAlgorithm.js +131 -0
- package/src/algorithms/LoadBalancingAlgorithm.js +23 -0
- package/src/algorithms/RoundRobinAlgorithm.js +67 -0
- package/src/config/ConfigManager.js +37 -0
- package/src/config/RouteLoader.js +36 -0
- package/src/core/Balancer.js +353 -0
- package/src/core/RoundRobinAlgorithm.js +60 -0
- package/src/core/ServerPool.js +77 -0
- package/src/dashboard/WebDashboard.js +150 -0
- package/src/dashboard/WebSocketServer.js +114 -0
- package/src/extensibility/CachingFilter.js +134 -0
- package/src/extensibility/FilterChain.js +93 -0
- package/src/extensibility/HookManager.js +48 -0
- package/src/protocol/HttpBalancer.js +37 -0
- package/src/protocol/HttpsBalancer.js +47 -0
- package/src/utils/BalancerLogger.js +102 -0
- package/src/utils/HealthChecker.js +51 -0
- package/src/utils/Logger.js +39 -0
- package/src/utils/MetricsCollector.js +82 -0
- package/src/utils/ProfessionalMetrics.js +501 -0
- package/start-iphash.sh +5 -0
- package/start-roundrobin.sh +5 -0
- package/stress-test.js +190 -0
- package/webpage/README.md +17 -0
- package/webpage/index.html +549 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const { LoadBalancingAlgorithm } = require('./LoadBalancingAlgorithm');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Algoritmo IPHash para balanceo de carga
|
|
5
|
+
* Asegura que las solicitudes del mismo cliente vayan siempre al mismo servidor
|
|
6
|
+
*/
|
|
7
|
+
class IPHashAlgorithm extends LoadBalancingAlgorithm {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
// Mapa para almacenar la asociación persistente IP -> índice de servidor
|
|
11
|
+
this.ipToServerIndexMap = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Selecciona un servidor usando el algoritmo IPHash
|
|
16
|
+
* @param {Array} servers - Lista de servidores disponibles
|
|
17
|
+
* @param {Object} requestContext - Contexto de la solicitud (contiene la IP del cliente)
|
|
18
|
+
* @returns {Object|null} Servidor seleccionado o null si no hay disponibles
|
|
19
|
+
*/
|
|
20
|
+
selectServer(servers, requestContext = {}) {
|
|
21
|
+
if (!servers || servers.length === 0) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Filtrar servidores activos y saludables
|
|
26
|
+
const availableServers = servers.filter(server =>
|
|
27
|
+
server.active !== false && server.healthy && server.failedAttempts < 5
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (availableServers.length === 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Obtener la IP del cliente del contexto de la solicitud
|
|
35
|
+
const clientIP = this.getClientIP(requestContext);
|
|
36
|
+
|
|
37
|
+
// Verificar si ya tenemos una asociación para esta IP
|
|
38
|
+
let serverIndex;
|
|
39
|
+
if (this.ipToServerIndexMap.has(clientIP)) {
|
|
40
|
+
serverIndex = this.ipToServerIndexMap.get(clientIP);
|
|
41
|
+
// Verificar que el servidor en ese índice aún esté disponible
|
|
42
|
+
if (serverIndex < availableServers.length) {
|
|
43
|
+
const cachedServer = availableServers[serverIndex];
|
|
44
|
+
if (cachedServer && cachedServer.healthy && cachedServer.active) {
|
|
45
|
+
// Servidor sigue disponible, usarlo
|
|
46
|
+
console.log(`IPHash: IP=${clientIP} reutilizando servidor previamente asignado (índice ${serverIndex})`);
|
|
47
|
+
return cachedServer;
|
|
48
|
+
} else {
|
|
49
|
+
// Servidor ya no está disponible, eliminar la asociación
|
|
50
|
+
this.ipToServerIndexMap.delete(clientIP);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Si no hay asociación o el servidor ya no está disponible, crear una nueva
|
|
56
|
+
const hash = this.simpleHash(clientIP);
|
|
57
|
+
serverIndex = hash % availableServers.length;
|
|
58
|
+
|
|
59
|
+
// Guardar la asociación para futuras solicitudes
|
|
60
|
+
this.ipToServerIndexMap.set(clientIP, serverIndex);
|
|
61
|
+
|
|
62
|
+
const selectedServer = availableServers[serverIndex];
|
|
63
|
+
|
|
64
|
+
// Log para mostrar el mapa de hash
|
|
65
|
+
console.log(`IPHash: IP=${clientIP}, Hash=${hash}, Asignando servidor índice ${serverIndex} (ID: ${selectedServer.id}, URL: ${selectedServer.url})`);
|
|
66
|
+
|
|
67
|
+
return selectedServer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Obtiene la IP del cliente del contexto de la solicitud
|
|
72
|
+
* @param {Object} requestContext - Contexto de la solicitud
|
|
73
|
+
* @returns {string} IP del cliente
|
|
74
|
+
*/
|
|
75
|
+
getClientIP(requestContext) {
|
|
76
|
+
// Buscar IP en diferentes posiciones posibles
|
|
77
|
+
const req = requestContext.req || {};
|
|
78
|
+
|
|
79
|
+
// Prioridad de fuentes de IP:
|
|
80
|
+
// 1. Cabecera X-Forwarded-For
|
|
81
|
+
// 2. Cabecera X-Real-IP
|
|
82
|
+
// 3. Remote Address
|
|
83
|
+
const forwardedFor = req.headers?.['x-forwarded-for'];
|
|
84
|
+
if (forwardedFor) {
|
|
85
|
+
// Tomar la primera IP de la lista (cliente original)
|
|
86
|
+
return forwardedFor.split(',')[0].trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const realIp = req.headers?.['x-real-ip'];
|
|
90
|
+
if (realIp) {
|
|
91
|
+
return realIp.trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// En entornos locales, connection.remoteAddress o socket.remoteAddress contendrán la IP
|
|
95
|
+
return req.connection?.remoteAddress ||
|
|
96
|
+
req.socket?.remoteAddress ||
|
|
97
|
+
'127.0.0.1';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Función hash simple para convertir una cadena en un número
|
|
102
|
+
* @param {string} str - Cadena a hashear
|
|
103
|
+
* @returns {number} Valor numérico del hash
|
|
104
|
+
*/
|
|
105
|
+
simpleHash(str) {
|
|
106
|
+
let hash = 0;
|
|
107
|
+
for (let i = 0; i < str.length; i++) {
|
|
108
|
+
const char = str.charCodeAt(i);
|
|
109
|
+
hash = ((hash << 5) - hash) + char;
|
|
110
|
+
hash = hash & hash; // Convertir a 32-bit integer
|
|
111
|
+
}
|
|
112
|
+
return Math.abs(hash);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Obtiene el mapa de IP a servidor para logging
|
|
117
|
+
* @returns {Map} Mapa de IPs a índices de servidores
|
|
118
|
+
*/
|
|
119
|
+
getIPToServerMap() {
|
|
120
|
+
return this.ipToServerIndexMap;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Nombre del algoritmo
|
|
125
|
+
*/
|
|
126
|
+
getName() {
|
|
127
|
+
return 'iphash';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { IPHashAlgorithm };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interfaz base para algoritmos de balanceo de carga
|
|
3
|
+
*/
|
|
4
|
+
class LoadBalancingAlgorithm {
|
|
5
|
+
/**
|
|
6
|
+
* Selecciona un servidor para manejar una solicitud
|
|
7
|
+
* @param {Array} servers - Lista de servidores disponibles
|
|
8
|
+
* @param {Object} requestContext - Contexto de la solicitud (opcional)
|
|
9
|
+
* @returns {Object|null} Servidor seleccionado o null si no hay disponibles
|
|
10
|
+
*/
|
|
11
|
+
selectServer(servers, requestContext = {}) {
|
|
12
|
+
throw new Error('Método selectServer debe ser implementado por la subclase');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Nombre del algoritmo
|
|
17
|
+
*/
|
|
18
|
+
getName() {
|
|
19
|
+
throw new Error('Método getName debe ser implementado por la subclase');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { LoadBalancingAlgorithm };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const { LoadBalancingAlgorithm } = require('./LoadBalancingAlgorithm');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Algoritmo RoundRobin para balanceo de carga
|
|
5
|
+
* Basado en la lógica mejorada implementada previamente en el balanceador
|
|
6
|
+
*/
|
|
7
|
+
class RoundRobinAlgorithm extends LoadBalancingAlgorithm {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
this.currentIndex = 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Selecciona un servidor usando el algoritmo RoundRobin
|
|
15
|
+
* @param {Array} servers - Lista de servidores disponibles
|
|
16
|
+
* @param {Object} requestContext - Contexto de la solicitud (opcional)
|
|
17
|
+
* @returns {Object|null} Servidor seleccionado o null si no hay disponibles
|
|
18
|
+
*/
|
|
19
|
+
selectServer(servers, requestContext = {}) {
|
|
20
|
+
if (!servers || servers.length === 0) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Filtrar servidores activos y saludables
|
|
25
|
+
const availableServers = servers.filter(server =>
|
|
26
|
+
server.active !== false && server.healthy && server.failedAttempts < 5
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (availableServers.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Intentar encontrar un servidor disponible usando round robin
|
|
34
|
+
// Si el servidor actual no está disponible, buscar el siguiente disponible
|
|
35
|
+
for (let i = 0; i < availableServers.length; i++) {
|
|
36
|
+
const candidateIndex = (this.currentIndex + i) % availableServers.length;
|
|
37
|
+
const candidateServer = availableServers[candidateIndex];
|
|
38
|
+
|
|
39
|
+
// Verificar si el servidor candidato está realmente disponible
|
|
40
|
+
if (this.isServerAvailable(candidateServer)) {
|
|
41
|
+
this.currentIndex = candidateIndex + 1; // Preparar para la próxima selección
|
|
42
|
+
return candidateServer;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Si ningún servidor está disponible, devolver null
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Método para verificar si un servidor está disponible
|
|
51
|
+
isServerAvailable(server) {
|
|
52
|
+
// Un servidor está disponible si:
|
|
53
|
+
// 1. Está marcado como healthy
|
|
54
|
+
// 2. Está activo
|
|
55
|
+
// 3. No ha superado el número máximo de intentos fallidos
|
|
56
|
+
return server.healthy && server.active && server.failedAttempts < 5;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Nombre del algoritmo
|
|
61
|
+
*/
|
|
62
|
+
getName() {
|
|
63
|
+
return 'roundrobin';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { RoundRobinAlgorithm };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class ConfigManager {
|
|
2
|
+
static instance = null;
|
|
3
|
+
|
|
4
|
+
constructor() {
|
|
5
|
+
if (ConfigManager.instance) {
|
|
6
|
+
return ConfigManager.instance;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
this.httpPort = parseInt(process.env.BALANCER_HTTP_PORT) || 8080;
|
|
10
|
+
this.httpsPort = process.env.BALANCER_HTTPS_PORT ? parseInt(process.env.BALANCER_HTTPS_PORT) : null;
|
|
11
|
+
this.configFilePath = process.env.CONFIG_FILE_PATH || './servers.json';
|
|
12
|
+
this.routesFilePath = process.env.ROUTES_FILE_PATH || './routes.json';
|
|
13
|
+
this.sslCertPath = process.env.SSL_CERT_PATH;
|
|
14
|
+
this.sslKeyPath = process.env.SSL_KEY_PATH;
|
|
15
|
+
this.healthCheckInterval = parseInt(process.env.HEALTH_CHECK_INTERVAL) || 30000;
|
|
16
|
+
this.logLevel = process.env.LOG_LEVEL || 'info';
|
|
17
|
+
|
|
18
|
+
ConfigManager.instance = this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static getInstance() {
|
|
22
|
+
if (!ConfigManager.instance) {
|
|
23
|
+
ConfigManager.instance = new ConfigManager();
|
|
24
|
+
}
|
|
25
|
+
return ConfigManager.instance;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get(key) {
|
|
29
|
+
return this[key];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
set(key, value) {
|
|
33
|
+
this[key] = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { ConfigManager };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
class RouteLoader {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.routes = [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
loadRoutesFromFile(filePath) {
|
|
9
|
+
if (fs.existsSync(filePath)) {
|
|
10
|
+
const routesData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
11
|
+
this.routes = routesData.routes || [];
|
|
12
|
+
return this.routes;
|
|
13
|
+
} else {
|
|
14
|
+
console.warn(`Archivo de rutas ${filePath} no encontrado`);
|
|
15
|
+
this.routes = [];
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getRoutes() {
|
|
21
|
+
return this.routes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
addRoute(route) {
|
|
25
|
+
this.routes.push(route);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
findMatchingRoute(url, method) {
|
|
29
|
+
return this.routes.find(route => {
|
|
30
|
+
const routeRegex = new RegExp(route.path.replace(/\*/g, '.*'));
|
|
31
|
+
return routeRegex.test(url) && (route.methods.includes(method) || route.methods.includes('*'));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { RouteLoader };
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* KUKUY
|
|
3
|
+
* Copyright (C) 2026 Desarrollador
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU General Public License
|
|
16
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const url = require('url');
|
|
21
|
+
const { HookManager } = require('../extensibility/HookManager');
|
|
22
|
+
const { FilterChain } = require('../extensibility/FilterChain');
|
|
23
|
+
const { ServerPool } = require('./ServerPool');
|
|
24
|
+
const { AlgorithmManager } = require('../algorithms/AlgorithmManager');
|
|
25
|
+
const { ConfigManager } = require('../config/ConfigManager');
|
|
26
|
+
const { Logger } = require('../utils/Logger');
|
|
27
|
+
const { BalancerLogger } = require('../utils/BalancerLogger');
|
|
28
|
+
const { RouteLoader } = require('../config/RouteLoader');
|
|
29
|
+
const { HttpBalancer } = require('../protocol/HttpBalancer');
|
|
30
|
+
const { HttpsBalancer } = require('../protocol/HttpsBalancer');
|
|
31
|
+
const { ProfessionalMetrics } = require('../utils/ProfessionalMetrics');
|
|
32
|
+
const { WebDashboard } = require('../dashboard/WebDashboard');
|
|
33
|
+
const { WebSocketServer } = require('../dashboard/WebSocketServer');
|
|
34
|
+
|
|
35
|
+
class Balancer {
|
|
36
|
+
constructor() {
|
|
37
|
+
this.config = ConfigManager.getInstance();
|
|
38
|
+
this.hookManager = new HookManager();
|
|
39
|
+
this.filterChain = new FilterChain();
|
|
40
|
+
this.serverPool = new ServerPool();
|
|
41
|
+
this.algorithmManager = new AlgorithmManager();
|
|
42
|
+
this.routeLoader = new RouteLoader();
|
|
43
|
+
this.logger = new Logger();
|
|
44
|
+
this.balancerLogger = new BalancerLogger();
|
|
45
|
+
this.metricsCollector = new ProfessionalMetrics(); // Usar métricas profesionales
|
|
46
|
+
this.startTime = Date.now();
|
|
47
|
+
|
|
48
|
+
// Cargar configuración
|
|
49
|
+
this.loadConfiguration();
|
|
50
|
+
|
|
51
|
+
// Configurar algoritmo de balanceo basado en la configuración
|
|
52
|
+
this.configureLoadBalancingAlgorithm();
|
|
53
|
+
|
|
54
|
+
// Inicializar panel web
|
|
55
|
+
this.webDashboard = new WebDashboard(this);
|
|
56
|
+
|
|
57
|
+
// Inicializar servidor WebSocket para datos en tiempo real
|
|
58
|
+
this.webSocketServer = new WebSocketServer(this);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
loadConfiguration() {
|
|
62
|
+
// Cargar servidores desde archivo de configuración
|
|
63
|
+
const configFile = process.env.CONFIG_FILE_PATH || './servers.json';
|
|
64
|
+
if (fs.existsSync(configFile)) {
|
|
65
|
+
const serversData = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
66
|
+
this.serverPool.addServers(serversData.servers);
|
|
67
|
+
} else {
|
|
68
|
+
this.logger.warn(`Archivo de configuración ${configFile} no encontrado`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Cargar rutas desde archivo de configuración usando RouteLoader
|
|
72
|
+
const routesFile = process.env.ROUTES_FILE_PATH || './routes.json';
|
|
73
|
+
this.routes = this.routeLoader.loadRoutesFromFile(routesFile);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
configureLoadBalancingAlgorithm() {
|
|
77
|
+
// Obtener el algoritmo de balanceo desde la configuración
|
|
78
|
+
const algorithmName = process.env.LOAD_BALANCING_ALGORITHM || 'roundrobin';
|
|
79
|
+
|
|
80
|
+
// Intentar configurar el algoritmo
|
|
81
|
+
const success = this.algorithmManager.setCurrentAlgorithm(algorithmName);
|
|
82
|
+
|
|
83
|
+
if (success) {
|
|
84
|
+
this.logger.info(`Algoritmo de balanceo configurado: ${algorithmName}`);
|
|
85
|
+
} else {
|
|
86
|
+
this.logger.warn(`Algoritmo desconocido: ${algorithmName}. Usando roundrobin por defecto.`);
|
|
87
|
+
this.algorithmManager.setCurrentAlgorithm('roundrobin');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
start() {
|
|
92
|
+
// Crear servidor HTTP usando HttpBalancer
|
|
93
|
+
if (this.config.httpPort) {
|
|
94
|
+
this.httpBalancer = new HttpBalancer(this.config.httpPort, this.handleRequest.bind(this));
|
|
95
|
+
this.httpServer = this.httpBalancer.start();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Crear servidor HTTPS usando HttpsBalancer si está configurado
|
|
99
|
+
if (this.config.httpsPort && this.config.sslCertPath && this.config.sslKeyPath) {
|
|
100
|
+
this.httpsBalancer = new HttpsBalancer(
|
|
101
|
+
this.config.httpsPort,
|
|
102
|
+
this.config.sslCertPath,
|
|
103
|
+
this.config.sslKeyPath,
|
|
104
|
+
this.handleRequest.bind(this)
|
|
105
|
+
);
|
|
106
|
+
this.httpsServer = this.httpsBalancer.start();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Iniciar panel web
|
|
110
|
+
this.webDashboard.start();
|
|
111
|
+
|
|
112
|
+
// Iniciar servidor WebSocket para datos en tiempo real
|
|
113
|
+
const wsPort = process.env.WEBSOCKET_PORT || 8083;
|
|
114
|
+
this.webSocketServer.start(wsPort);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async handleRequest(clientReq, clientRes) {
|
|
118
|
+
try {
|
|
119
|
+
// Aplicar filters antes de procesar la solicitud
|
|
120
|
+
const filterData = { req: clientReq, res: clientRes, allowed: true, statusCode: 200, message: '' };
|
|
121
|
+
const filteredResult = await this.filterChain.applyFilters('request', clientReq, clientRes);
|
|
122
|
+
|
|
123
|
+
if (!filteredResult || !filteredResult.allowed) {
|
|
124
|
+
const statusCode = filteredResult?.statusCode || 403;
|
|
125
|
+
const message = filteredResult?.message || 'Forbidden';
|
|
126
|
+
clientRes.writeHead(statusCode);
|
|
127
|
+
clientRes.end(message);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Emitir hook cuando se recibe una solicitud
|
|
132
|
+
await this.hookManager.executeHooks('onRequestReceived', { req: clientReq, res: clientRes });
|
|
133
|
+
|
|
134
|
+
// Determinar la ruta objetivo
|
|
135
|
+
const targetServer = await this.getTargetServer(clientReq.url, clientReq.method, clientReq);
|
|
136
|
+
|
|
137
|
+
if (!targetServer) {
|
|
138
|
+
clientRes.writeHead(404);
|
|
139
|
+
clientRes.end('No available servers');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Emitir hook después de seleccionar servidor
|
|
144
|
+
await this.hookManager.executeHooks('onServerSelected', {
|
|
145
|
+
req: clientReq,
|
|
146
|
+
res: clientRes,
|
|
147
|
+
server: targetServer
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Reenviar solicitud al servidor backend
|
|
151
|
+
this.proxyRequest(clientReq, clientRes, targetServer);
|
|
152
|
+
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.logger.error(`Error manejando solicitud: ${error.message}`);
|
|
155
|
+
|
|
156
|
+
// Emitir hook cuando ocurre un error
|
|
157
|
+
await this.hookManager.executeHooks('onServerError', {
|
|
158
|
+
req: clientReq,
|
|
159
|
+
res: clientRes,
|
|
160
|
+
error
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
clientRes.writeHead(500);
|
|
164
|
+
clientRes.end('Internal Server Error');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async getTargetServer(requestUrl, method, clientReq = null) {
|
|
169
|
+
// Usar RouteLoader para encontrar la ruta coincidente
|
|
170
|
+
const matchedRoute = this.routeLoader.findMatchingRoute(requestUrl, method);
|
|
171
|
+
|
|
172
|
+
// Crear contexto de solicitud para algoritmos que lo necesiten (como IPHash)
|
|
173
|
+
const requestContext = {
|
|
174
|
+
req: clientReq,
|
|
175
|
+
url: requestUrl,
|
|
176
|
+
method: method
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
if (matchedRoute && matchedRoute.target) {
|
|
180
|
+
// Si hay una ruta específica, usar solo los servidores definidos para esa ruta
|
|
181
|
+
const routeServers = this.serverPool.getHealthyServers().filter(server =>
|
|
182
|
+
server.tags && server.tags.includes(matchedRoute.target)
|
|
183
|
+
);
|
|
184
|
+
if (routeServers.length > 0) {
|
|
185
|
+
return this.algorithmManager.selectServer(routeServers, requestContext);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Si no hay ruta específica o no hay servidores para esa ruta, usar todos los servidores
|
|
190
|
+
const allServers = this.serverPool.getHealthyServers();
|
|
191
|
+
return this.algorithmManager.selectServer(allServers, requestContext);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async proxyRequest(clientReq, clientRes, initialTargetServer) {
|
|
195
|
+
const startTime = Date.now();
|
|
196
|
+
let targetServer = initialTargetServer;
|
|
197
|
+
let retryCount = 0;
|
|
198
|
+
const maxRetries = this.serverPool.getHealthyServers().length; // Máximo de reintentos según servidores saludables
|
|
199
|
+
|
|
200
|
+
// Intentar enviar la solicitud con reintento si el servidor inicial falla
|
|
201
|
+
while (retryCount <= maxRetries) {
|
|
202
|
+
try {
|
|
203
|
+
const parsedUrl = url.parse(targetServer.url);
|
|
204
|
+
const targetOptions = {
|
|
205
|
+
hostname: parsedUrl.hostname,
|
|
206
|
+
port: parsedUrl.port,
|
|
207
|
+
path: clientReq.url,
|
|
208
|
+
method: clientReq.method,
|
|
209
|
+
headers: clientReq.headers
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const httpModule = targetServer.protocol === 'https:' ? require('https') : require('http');
|
|
213
|
+
const proxyReq = httpModule.request(targetOptions);
|
|
214
|
+
|
|
215
|
+
// Registrar intento de conexión a servidor target
|
|
216
|
+
this.balancerLogger.logTargetSelection(targetServer, 'round_robin');
|
|
217
|
+
|
|
218
|
+
// Enviar cuerpo de la solicitud
|
|
219
|
+
clientReq.pipe(proxyReq);
|
|
220
|
+
|
|
221
|
+
// Resolver la promesa cuando se complete la solicitud
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
proxyReq.on('response', (serverRes) => {
|
|
224
|
+
// Copiar headers de la respuesta
|
|
225
|
+
clientRes.writeHead(serverRes.statusCode, serverRes.headers);
|
|
226
|
+
|
|
227
|
+
// Enviar cuerpo de la respuesta al cliente
|
|
228
|
+
serverRes.pipe(clientRes);
|
|
229
|
+
|
|
230
|
+
// Calcular tiempo de respuesta
|
|
231
|
+
const responseTime = Date.now() - startTime;
|
|
232
|
+
|
|
233
|
+
// Registrar métricas profesionales
|
|
234
|
+
this.metricsCollector.recordRequest(
|
|
235
|
+
clientReq.method,
|
|
236
|
+
clientReq.url,
|
|
237
|
+
targetServer.id,
|
|
238
|
+
responseTime,
|
|
239
|
+
serverRes.statusCode,
|
|
240
|
+
serverRes.statusCode < 400
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Registrar servidor online
|
|
244
|
+
this.balancerLogger.logOnlineTarget(targetServer, {
|
|
245
|
+
url: clientReq.url,
|
|
246
|
+
method: clientReq.method,
|
|
247
|
+
responseTime,
|
|
248
|
+
statusCode: serverRes.statusCode
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Actualizar estado del servidor en las métricas
|
|
252
|
+
this.metricsCollector.updateServerStatus(targetServer.id, 'online');
|
|
253
|
+
|
|
254
|
+
// Emitir hook cuando se envía la respuesta
|
|
255
|
+
this.hookManager.executeHooks('onResponseSent', {
|
|
256
|
+
req: clientReq,
|
|
257
|
+
res: clientRes,
|
|
258
|
+
serverRes,
|
|
259
|
+
server: targetServer,
|
|
260
|
+
responseTime
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
resolve(); // Resolver la promesa al completar la solicitud
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
proxyReq.on('error', async (err) => {
|
|
267
|
+
const responseTime = Date.now() - startTime;
|
|
268
|
+
|
|
269
|
+
this.logger.error(`Error conectando con servidor ${targetServer.url}: ${err.message}`);
|
|
270
|
+
|
|
271
|
+
// Registrar servidor offline
|
|
272
|
+
this.balancerLogger.logOfflineTarget(targetServer, err, {
|
|
273
|
+
url: clientReq.url,
|
|
274
|
+
method: clientReq.method,
|
|
275
|
+
responseTime
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Actualizar estado del servidor en las métricas
|
|
279
|
+
this.metricsCollector.updateServerStatus(targetServer.id, 'offline');
|
|
280
|
+
|
|
281
|
+
// Registrar métricas profesionales para error
|
|
282
|
+
this.metricsCollector.recordRequest(
|
|
283
|
+
clientReq.method,
|
|
284
|
+
clientReq.url,
|
|
285
|
+
targetServer.id,
|
|
286
|
+
responseTime,
|
|
287
|
+
502,
|
|
288
|
+
false
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Marcar servidor como fallido
|
|
292
|
+
this.serverPool.markServerAsFailed(targetServer.id);
|
|
293
|
+
|
|
294
|
+
// Emitir hook cuando ocurre un error con el servidor
|
|
295
|
+
await this.hookManager.executeHooks('onServerError', {
|
|
296
|
+
req: clientReq,
|
|
297
|
+
res: clientRes,
|
|
298
|
+
server: targetServer,
|
|
299
|
+
error: err,
|
|
300
|
+
responseTime
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Si hay más servidores disponibles, reintentar con otro servidor
|
|
304
|
+
if (retryCount < maxRetries) {
|
|
305
|
+
retryCount++;
|
|
306
|
+
|
|
307
|
+
// Obtener un nuevo servidor objetivo (excluyendo el que falló)
|
|
308
|
+
const availableServers = this.serverPool.getHealthyServers().filter(s => s.id !== targetServer.id);
|
|
309
|
+
if (availableServers.length > 0) {
|
|
310
|
+
// Seleccionar un nuevo servidor
|
|
311
|
+
targetServer = availableServers[(retryCount - 1) % availableServers.length];
|
|
312
|
+
console.log(`Reintentando con servidor alternativo: ${targetServer.url}`);
|
|
313
|
+
|
|
314
|
+
// Volver a intentar la solicitud con el nuevo servidor
|
|
315
|
+
setTimeout(() => {
|
|
316
|
+
this.proxyRequest(clientReq, clientRes, targetServer).then(resolve).catch(reject);
|
|
317
|
+
}, 10); // Pequeña pausa antes de reintentar
|
|
318
|
+
|
|
319
|
+
return; // Salir para evitar resolver/rechazar la promesa dos veces
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Si no hay más reintentos posibles, enviar error al cliente
|
|
324
|
+
if (!clientRes.headersSent) {
|
|
325
|
+
clientRes.writeHead(502);
|
|
326
|
+
clientRes.end('Bad Gateway');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
resolve(); // Resolver para evitar múltiples llamadas
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error('Error en proxyRequest:', error);
|
|
334
|
+
if (!clientRes.headersSent) {
|
|
335
|
+
clientRes.writeHead(500);
|
|
336
|
+
clientRes.end('Internal Server Error');
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
stop() {
|
|
344
|
+
if (this.httpBalancer) {
|
|
345
|
+
this.httpBalancer.stop();
|
|
346
|
+
}
|
|
347
|
+
if (this.httpsBalancer) {
|
|
348
|
+
this.httpsBalancer.stop();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = { Balancer };
|