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.
Files changed (41) hide show
  1. package/.ctagsd/ctagsd.json +954 -0
  2. package/.ctagsd/file_list.txt +100 -0
  3. package/.ctagsd/tags.db +0 -0
  4. package/CHANGELOG.md +101 -0
  5. package/LICENSE +680 -0
  6. package/README.md +251 -0
  7. package/captura.png +0 -0
  8. package/kukuy.js +23 -0
  9. package/kukuy.workspace +11 -0
  10. package/package.json +26 -0
  11. package/restart-balancer.sh +10 -0
  12. package/routes.json +14 -0
  13. package/scripts/load_test.py +151 -0
  14. package/servers.json +19 -0
  15. package/servers_real.json +19 -0
  16. package/src/algorithms/AlgorithmManager.js +85 -0
  17. package/src/algorithms/IPHashAlgorithm.js +131 -0
  18. package/src/algorithms/LoadBalancingAlgorithm.js +23 -0
  19. package/src/algorithms/RoundRobinAlgorithm.js +67 -0
  20. package/src/config/ConfigManager.js +37 -0
  21. package/src/config/RouteLoader.js +36 -0
  22. package/src/core/Balancer.js +353 -0
  23. package/src/core/RoundRobinAlgorithm.js +60 -0
  24. package/src/core/ServerPool.js +77 -0
  25. package/src/dashboard/WebDashboard.js +150 -0
  26. package/src/dashboard/WebSocketServer.js +114 -0
  27. package/src/extensibility/CachingFilter.js +134 -0
  28. package/src/extensibility/FilterChain.js +93 -0
  29. package/src/extensibility/HookManager.js +48 -0
  30. package/src/protocol/HttpBalancer.js +37 -0
  31. package/src/protocol/HttpsBalancer.js +47 -0
  32. package/src/utils/BalancerLogger.js +102 -0
  33. package/src/utils/HealthChecker.js +51 -0
  34. package/src/utils/Logger.js +39 -0
  35. package/src/utils/MetricsCollector.js +82 -0
  36. package/src/utils/ProfessionalMetrics.js +501 -0
  37. package/start-iphash.sh +5 -0
  38. package/start-roundrobin.sh +5 -0
  39. package/stress-test.js +190 -0
  40. package/webpage/README.md +17 -0
  41. 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 };