kukuy 1.4.0 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kukuy",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Balanceador de carga Backend",
5
5
  "main": "kukuy.js",
6
6
  "scripts": {
@@ -9,6 +9,7 @@
9
9
  "test": "echo \"Error: no test specified\" && exit 1"
10
10
  },
11
11
  "keywords": [
12
+ "backend",
12
13
  "load-balancer",
13
14
  "round-robin",
14
15
  "ip-hash",
@@ -22,5 +23,10 @@
22
23
  "jerk-hooked-lib": "^2.0.0",
23
24
  "ws": "^8.19.0"
24
25
  },
25
- "type": "commonjs"
26
+ "type": "commonjs",
27
+ "homepage": "https://bsanchez.unaux.com/",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://gitlab.com/bytedogssyndicate1/kukuy"
31
+ }
26
32
  }
package/servers_real.json CHANGED
@@ -14,6 +14,11 @@
14
14
  "url": "http://localhost:5445",
15
15
  "weight": 1,
16
16
  "tags": ["backend"]
17
+ },
18
+ {
19
+ "url": "http://localhost:6666",
20
+ "weight": 1,
21
+ "tags": ["backend"]
17
22
  }
18
23
  ]
19
24
  }
@@ -9,6 +9,10 @@ class IPHashAlgorithm extends LoadBalancingAlgorithm {
9
9
  super();
10
10
  // Mapa para almacenar la asociación persistente IP -> índice de servidor
11
11
  this.ipToServerIndexMap = new Map();
12
+ // Caché para servidores filtrados
13
+ this.filteredServersCache = new Map();
14
+ this.cacheTimestamp = 0;
15
+ this.cacheValidity = 1000; // 1 segundo de validez de caché
12
16
  }
13
17
 
14
18
  /**
@@ -22,10 +26,23 @@ class IPHashAlgorithm extends LoadBalancingAlgorithm {
22
26
  return null;
23
27
  }
24
28
 
25
- // Filtrar servidores activos y saludables
26
- const availableServers = servers.filter(server =>
27
- server.active !== false && server.healthy && server.failedAttempts < 5
28
- );
29
+ // Obtener timestamp actual
30
+ const now = Date.now();
31
+
32
+ // Verificar si la caché es válida
33
+ let availableServers;
34
+ if (now - this.cacheTimestamp > this.cacheValidity || !this.filteredServersCache.has(servers)) {
35
+ // Filtrar servidores activos y saludables
36
+ availableServers = servers.filter(server =>
37
+ server.active !== false && server.healthy && server.failedAttempts < 5
38
+ );
39
+
40
+ // Actualizar caché
41
+ this.filteredServersCache.set(servers, availableServers);
42
+ this.cacheTimestamp = now;
43
+ } else {
44
+ availableServers = this.filteredServersCache.get(servers);
45
+ }
29
46
 
30
47
  if (availableServers.length === 0) {
31
48
  return null;
@@ -43,7 +60,6 @@ class IPHashAlgorithm extends LoadBalancingAlgorithm {
43
60
  const cachedServer = availableServers[serverIndex];
44
61
  if (cachedServer && cachedServer.healthy && cachedServer.active) {
45
62
  // Servidor sigue disponible, usarlo
46
- console.log(`IPHash: IP=${clientIP} reutilizando servidor previamente asignado (índice ${serverIndex})`);
47
63
  return cachedServer;
48
64
  } else {
49
65
  // Servidor ya no está disponible, eliminar la asociación
@@ -61,9 +77,6 @@ class IPHashAlgorithm extends LoadBalancingAlgorithm {
61
77
 
62
78
  const selectedServer = availableServers[serverIndex];
63
79
 
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
80
  return selectedServer;
68
81
  }
69
82
 
@@ -104,12 +117,11 @@ class IPHashAlgorithm extends LoadBalancingAlgorithm {
104
117
  */
105
118
  simpleHash(str) {
106
119
  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
120
+ const len = str.length;
121
+ for (let i = 0; i < len; i++) {
122
+ hash = ((hash << 5) - hash) + str.charCodeAt(i);
111
123
  }
112
- return Math.abs(hash);
124
+ return Math.abs(hash | 0); // Convertir a 32-bit integer
113
125
  }
114
126
 
115
127
  /**
@@ -8,6 +8,10 @@ class RoundRobinAlgorithm extends LoadBalancingAlgorithm {
8
8
  constructor() {
9
9
  super();
10
10
  this.currentIndex = 0;
11
+ // Caché para servidores filtrados
12
+ this.filteredServersCache = new Map();
13
+ this.cacheTimestamp = 0;
14
+ this.cacheValidity = 1000; // 1 segundo de validez de caché
11
15
  }
12
16
 
13
17
  /**
@@ -21,39 +25,33 @@ class RoundRobinAlgorithm extends LoadBalancingAlgorithm {
21
25
  return null;
22
26
  }
23
27
 
24
- // Filtrar servidores activos y saludables
25
- const availableServers = servers.filter(server =>
26
- server.active !== false && server.healthy && server.failedAttempts < 5
27
- );
28
+ // Obtener timestamp actual
29
+ const now = Date.now();
30
+
31
+ // Verificar si la caché es válida
32
+ let availableServers;
33
+ if (now - this.cacheTimestamp > this.cacheValidity || !this.filteredServersCache.has(servers)) {
34
+ // Filtrar servidores activos y saludables
35
+ availableServers = servers.filter(server =>
36
+ server.active !== false && server.healthy && server.failedAttempts < 5
37
+ );
38
+
39
+ // Actualizar caché
40
+ this.filteredServersCache.set(servers, availableServers);
41
+ this.cacheTimestamp = now;
42
+ } else {
43
+ availableServers = this.filteredServersCache.get(servers);
44
+ }
28
45
 
29
46
  if (availableServers.length === 0) {
30
47
  return null;
31
48
  }
32
49
 
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
- }
50
+ // Obtener servidor usando round robin sin bucle
51
+ const server = availableServers[this.currentIndex % availableServers.length];
52
+ this.currentIndex = (this.currentIndex + 1) % availableServers.length;
49
53
 
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;
54
+ return server;
57
55
  }
58
56
 
59
57
  /**
@@ -18,8 +18,8 @@
18
18
 
19
19
  const fs = require('fs');
20
20
  const url = require('url');
21
- const { HookManager } = require('../extensibility/HookManager');
22
- const { FilterChain } = require('../extensibility/FilterChain');
21
+ const { ExtendedHookManager } = require('../extensibility/ExtendedHookManager');
22
+ const { ExtendedFilterChain } = require('../extensibility/ExtendedFilterChain');
23
23
  const { ServerPool } = require('./ServerPool');
24
24
  const { AlgorithmManager } = require('../algorithms/AlgorithmManager');
25
25
  const { ConfigManager } = require('../config/ConfigManager');
@@ -31,12 +31,14 @@ const { HttpsBalancer } = require('../protocol/HttpsBalancer');
31
31
  const { ProfessionalMetrics } = require('../utils/ProfessionalMetrics');
32
32
  const { WebDashboard } = require('../dashboard/WebDashboard');
33
33
  const { WebSocketServer } = require('../dashboard/WebSocketServer');
34
+ const { PostStartupExtension } = require('../extensibility/PostStartupExtension');
35
+ const { PluginManager } = require('../plugins/PluginManager');
34
36
 
35
37
  class Balancer {
36
38
  constructor() {
37
39
  this.config = ConfigManager.getInstance();
38
- this.hookManager = new HookManager();
39
- this.filterChain = new FilterChain();
40
+ this.hookManager = new ExtendedHookManager();
41
+ this.filterChain = new ExtendedFilterChain();
40
42
  this.serverPool = new ServerPool();
41
43
  this.algorithmManager = new AlgorithmManager();
42
44
  this.routeLoader = new RouteLoader();
@@ -56,6 +58,64 @@ class Balancer {
56
58
 
57
59
  // Inicializar servidor WebSocket para datos en tiempo real
58
60
  this.webSocketServer = new WebSocketServer(this);
61
+
62
+ // Inicializar sistema de extensión post-inicio
63
+ this.postStartupExtension = new PostStartupExtension(this);
64
+
65
+ // Activar plugin de depuración si está habilitado
66
+ this.initializeDebugPlugin();
67
+
68
+ // Inicializar sistema de plugins
69
+ this.pluginManager = new PluginManager(this);
70
+ }
71
+
72
+ initializeDebugPlugin() {
73
+ // Verificar si el plugin de depuración está habilitado
74
+ const debugEnabled = process.env.DEBUG_REQUEST_HEADERS === 'true' ||
75
+ process.env.KUKUY_DEBUG_HEADERS === 'true';
76
+
77
+ if (debugEnabled) {
78
+ // Registrar hooks para mostrar encabezados y servidor de destino
79
+
80
+ // Hook para mostrar encabezados cuando se recibe una solicitud
81
+ this.hookManager.addHook('onRequestReceived', ({ req, res }) => {
82
+ console.log('\n--- Nueva Solicitud Recibida ---');
83
+ console.log(`Método: ${req.method}`);
84
+ console.log(`URL: ${req.url}`);
85
+ console.log('Encabezados:');
86
+
87
+ // Mostrar todos los encabezados de la solicitud
88
+ for (const [header, value] of Object.entries(req.headers)) {
89
+ console.log(` ${header}: ${value}`);
90
+ }
91
+
92
+ console.log('-----------------------------\n');
93
+ });
94
+
95
+ // Hook para mostrar el servidor de destino asignado
96
+ this.hookManager.addHook('onServerSelected', ({ req, res, server }) => {
97
+ console.log('\n--- Servidor de Destino Asignado ---');
98
+ console.log(`Solicitud: ${req.method} ${req.url}`);
99
+ console.log(`Servidor destino: ${server.url}`);
100
+ console.log(`ID del servidor: ${server.id}`);
101
+ console.log(`Protocolo: ${server.protocol}`);
102
+ console.log(`Host: ${server.host}`);
103
+ console.log(`Etiquetas: ${server.tags.join(', ') || 'ninguna'}`);
104
+ console.log('----------------------------------\n');
105
+ });
106
+
107
+ // Hook para mostrar información cuando se envía la respuesta
108
+ this.hookManager.addHook('onResponseSent', ({ req, res, serverRes, responseTime }) => {
109
+ console.log('\n--- Respuesta Enviada ---');
110
+ console.log(`Solicitud: ${req.method} ${req.url}`);
111
+ console.log(`Código de estado: ${serverRes.statusCode}`);
112
+ console.log(`Tiempo de respuesta: ${responseTime}ms`);
113
+ console.log('------------------------\n');
114
+ });
115
+
116
+ console.log('Plugin de depuración de solicitud activado');
117
+ console.log('Mostrando encabezados de solicitud y servidores de destino en stdout');
118
+ }
59
119
  }
60
120
 
61
121
  loadConfiguration() {
@@ -88,7 +148,7 @@ class Balancer {
88
148
  }
89
149
  }
90
150
 
91
- start() {
151
+ async start() {
92
152
  // Crear servidor HTTP usando HttpBalancer
93
153
  if (this.config.httpPort) {
94
154
  this.httpBalancer = new HttpBalancer(this.config.httpPort, this.handleRequest.bind(this));
@@ -112,6 +172,9 @@ class Balancer {
112
172
  // Iniciar servidor WebSocket para datos en tiempo real
113
173
  const wsPort = process.env.WEBSOCKET_PORT || 8083;
114
174
  this.webSocketServer.start(wsPort);
175
+
176
+ // Cargar plugins después de iniciar componentes principales
177
+ await this.pluginManager.loadPlugins();
115
178
  }
116
179
 
117
180
  async handleRequest(clientReq, clientRes) {
@@ -178,11 +241,12 @@ class Balancer {
178
241
 
179
242
  if (matchedRoute && matchedRoute.target) {
180
243
  // 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);
244
+ const routeServers = this.serverPool.getServersByTag(matchedRoute.target);
245
+ if (routeServers && routeServers.length > 0) {
246
+ const healthyRouteServers = routeServers.filter(server => server.healthy && server.active);
247
+ if (healthyRouteServers.length > 0) {
248
+ return this.algorithmManager.selectServer(healthyRouteServers, requestContext);
249
+ }
186
250
  }
187
251
  }
188
252
 
@@ -197,10 +261,10 @@ class Balancer {
197
261
  let retryCount = 0;
198
262
  const maxRetries = this.serverPool.getHealthyServers().length; // Máximo de reintentos según servidores saludables
199
263
 
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);
264
+ // Función auxiliar para manejar la solicitud
265
+ const makeRequest = (server) => {
266
+ return new Promise((resolve, reject) => {
267
+ const parsedUrl = url.parse(server.url);
204
268
  const targetOptions = {
205
269
  hostname: parsedUrl.hostname,
206
270
  port: parsedUrl.port,
@@ -209,133 +273,134 @@ class Balancer {
209
273
  headers: clientReq.headers
210
274
  };
211
275
 
212
- const httpModule = targetServer.protocol === 'https:' ? require('https') : require('http');
276
+ const httpModule = server.protocol === 'https:' ? require('https') : require('http');
213
277
  const proxyReq = httpModule.request(targetOptions);
214
278
 
215
279
  // Registrar intento de conexión a servidor target
216
- this.balancerLogger.logTargetSelection(targetServer, 'round_robin');
280
+ this.balancerLogger.logTargetSelection(server, 'round_robin');
217
281
 
218
282
  // Enviar cuerpo de la solicitud
219
283
  clientReq.pipe(proxyReq);
220
284
 
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
285
+ proxyReq.on('response', (serverRes) => {
286
+ // Copiar headers de la respuesta
287
+ clientRes.writeHead(serverRes.statusCode, serverRes.headers);
288
+
289
+ // Enviar cuerpo de la respuesta al cliente
290
+ serverRes.pipe(clientRes);
291
+
292
+ // Calcular tiempo de respuesta
293
+ const responseTime = Date.now() - startTime;
294
+
295
+ // Registrar métricas profesionales
296
+ this.metricsCollector.recordRequest(
297
+ clientReq.method,
298
+ clientReq.url,
299
+ server.id,
300
+ responseTime,
301
+ serverRes.statusCode,
302
+ serverRes.statusCode < 400
303
+ );
304
+
305
+ // Registrar servidor online
306
+ this.balancerLogger.logOnlineTarget(server, {
307
+ url: clientReq.url,
308
+ method: clientReq.method,
309
+ responseTime,
310
+ statusCode: serverRes.statusCode
264
311
  });
265
312
 
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
- }
313
+ // Actualizar estado del servidor en las métricas
314
+ this.metricsCollector.updateServerStatus(server.id, 'online');
322
315
 
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
- }
316
+ // Emitir hook cuando se envía la respuesta
317
+ this.hookManager.executeHooks('onResponseSent', {
318
+ req: clientReq,
319
+ res: clientRes,
320
+ serverRes,
321
+ server,
322
+ responseTime
323
+ });
328
324
 
329
- resolve(); // Resolver para evitar múltiples llamadas
325
+ resolve(); // Resolver la promesa al completar la solicitud
326
+ });
327
+
328
+ proxyReq.on('error', async (err) => {
329
+ const responseTime = Date.now() - startTime;
330
+
331
+ this.logger.error(`Error conectando con servidor ${server.url}: ${err.message}`);
332
+
333
+ // Registrar servidor offline
334
+ this.balancerLogger.logOfflineTarget(server, err, {
335
+ url: clientReq.url,
336
+ method: clientReq.method,
337
+ responseTime
338
+ });
339
+
340
+ // Actualizar estado del servidor en las métricas
341
+ this.metricsCollector.updateServerStatus(server.id, 'offline');
342
+
343
+ // Registrar métricas profesionales para error
344
+ this.metricsCollector.recordRequest(
345
+ clientReq.method,
346
+ clientReq.url,
347
+ server.id,
348
+ responseTime,
349
+ 502,
350
+ false
351
+ );
352
+
353
+ // Marcar servidor como fallido
354
+ this.serverPool.markServerAsFailed(server.id);
355
+
356
+ // Emitir hook cuando ocurre un error con el servidor
357
+ await this.hookManager.executeHooks('onServerError', {
358
+ req: clientReq,
359
+ res: clientRes,
360
+ server,
361
+ error: err,
362
+ responseTime
330
363
  });
364
+
365
+ // Si hay más servidores disponibles, reintentar con otro servidor
366
+ if (retryCount < maxRetries) {
367
+ retryCount++;
368
+
369
+ // Obtener un nuevo servidor objetivo (excluyendo los que ya fallaron)
370
+ const availableServers = this.serverPool.getHealthyServers().filter(s => s.id !== server.id);
371
+ if (availableServers.length > 0) {
372
+ // Seleccionar un nuevo servidor
373
+ const nextServer = availableServers[(retryCount - 1) % availableServers.length];
374
+ console.log(`Reintentando con servidor alternativo: ${nextServer.url}`);
375
+
376
+ // Volver a intentar la solicitud con el nuevo servidor
377
+ setTimeout(() => {
378
+ makeRequest(nextServer).then(resolve).catch(reject);
379
+ }, 10); // Pequeña pausa antes de reintentar
380
+
381
+ return; // Salir para evitar resolver/rechazar la promesa dos veces
382
+ }
383
+ }
384
+
385
+ // Si no hay más reintentos posibles, enviar error al cliente
386
+ if (!clientRes.headersSent) {
387
+ clientRes.writeHead(502);
388
+ clientRes.end('Bad Gateway');
389
+ }
390
+
391
+ resolve(); // Resolver para evitar múltiples llamadas
331
392
  });
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;
393
+ });
394
+ };
395
+
396
+ // Intentar enviar la solicitud con reintento si el servidor inicial falla
397
+ try {
398
+ await makeRequest(targetServer);
399
+ } catch (error) {
400
+ console.error('Error en proxyRequest:', error);
401
+ if (!clientRes.headersSent) {
402
+ clientRes.writeHead(500);
403
+ clientRes.end('Internal Server Error');
339
404
  }
340
405
  }
341
406
  }
@@ -348,6 +413,14 @@ class Balancer {
348
413
  this.httpsBalancer.stop();
349
414
  }
350
415
  }
416
+
417
+ /**
418
+ * Get the post-startup extension API
419
+ * @returns {PostStartupExtension} The extension API
420
+ */
421
+ getPostStartupExtension() {
422
+ return this.postStartupExtension;
423
+ }
351
424
  }
352
425
 
353
426
  module.exports = { Balancer };