kukuy 1.6.0 → 1.9.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/README.md +59 -3
- package/balancer.log +28 -0
- package/kukuy-plugins/README.md +77 -43
- package/kukuy-plugins/cache-plugin/index.js +477 -0
- package/kukuy-plugins/cache-plugin/manifest.json +17 -0
- package/kukuy-plugins/ejemplo-plugin/index.js +7 -5
- package/kukuy-plugins/ejemplo-plugin/manifest.json +2 -2
- package/kukuy-plugins/health-checker/index.js +168 -0
- package/kukuy-plugins/health-checker/manifest.json +16 -0
- package/kukuy-plugins/health-monitor/index.js +58 -0
- package/kukuy-plugins/health-monitor/manifest.json +16 -0
- package/kukuy-plugins/redirect-plugin/index.js +172 -0
- package/kukuy-plugins/redirect-plugin/manifest.json +15 -0
- package/package.json +7 -4
- package/src/core/Balancer.js +100 -34
- package/src/core/ServerPool.js +2 -2
- package/src/extensibility/FilterChain.js +2 -9
- package/src/extensibility/HookManager.js +1 -0
- package/src/plugins/PluginManager.js +48 -0
- package/src/utils/HealthChecker.js +61 -6
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
// Almacenamiento de la caché en memoria
|
|
4
|
+
let cacheStorage = new Map();
|
|
5
|
+
let cacheMetadata = new Map(); // Almacenar metadatos como TTL, tamaño, etc.
|
|
6
|
+
|
|
7
|
+
// Configuración del plugin
|
|
8
|
+
let pluginConfig = {
|
|
9
|
+
maxCacheSize: 100, // Número máximo de entradas
|
|
10
|
+
defaultTTL: 300000, // Tiempo de vida por defecto (5 minutos en ms)
|
|
11
|
+
enableCompression: false,
|
|
12
|
+
cacheableMethods: ['GET'],
|
|
13
|
+
cacheableStatusCodes: [200, 201, 204]
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Variable para almacenar temporalmente las solicitudes que están siendo procesadas
|
|
17
|
+
let activeRequests = new Map();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Inicializa el plugin de cache
|
|
21
|
+
* @param {Object} balancer - Instancia del balanceador
|
|
22
|
+
*/
|
|
23
|
+
async function init(balancer) {
|
|
24
|
+
console.log('Inicializando plugin de cache robusto...');
|
|
25
|
+
|
|
26
|
+
const extension = balancer.getPostStartupExtension();
|
|
27
|
+
|
|
28
|
+
// Registrar filtro para procesar solicitudes con la máxima prioridad
|
|
29
|
+
extension.registerFilter('request_processing', requestProcessingFilter, 0);
|
|
30
|
+
|
|
31
|
+
// Registrar hook para cuando se recibe una solicitud
|
|
32
|
+
extension.registerHook('onRequestReceived', onRequestReceivedHook, 0);
|
|
33
|
+
|
|
34
|
+
// Registrar hook para cuando se selecciona un servidor
|
|
35
|
+
extension.registerHook('onServerSelected', onServerSelectedHook, 0);
|
|
36
|
+
|
|
37
|
+
// Registrar hook para cuando la respuesta está lista para ser enviada
|
|
38
|
+
extension.registerHook('onResponseReady', onResponseReadyHook, 0);
|
|
39
|
+
|
|
40
|
+
// Registrar hook para cuando se envía la respuesta
|
|
41
|
+
extension.registerHook('onResponseSent', onResponseSentHook, 0);
|
|
42
|
+
|
|
43
|
+
// Registrar hook para cuando ocurre un error con el servidor
|
|
44
|
+
extension.registerHook('onServerError', onServerErrorHook, 0);
|
|
45
|
+
|
|
46
|
+
// Configurar el plugin con valores del entorno
|
|
47
|
+
configurePluginFromEnv();
|
|
48
|
+
|
|
49
|
+
// Iniciar tarea de limpieza periódica de entradas expiradas
|
|
50
|
+
startCacheCleanupTask();
|
|
51
|
+
|
|
52
|
+
console.log('Plugin de cache robusto inicializado correctamente');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Configura el plugin con valores del entorno
|
|
57
|
+
*/
|
|
58
|
+
function configurePluginFromEnv() {
|
|
59
|
+
if (process.env.CACHE_MAX_SIZE) {
|
|
60
|
+
pluginConfig.maxCacheSize = parseInt(process.env.CACHE_MAX_SIZE) || pluginConfig.maxCacheSize;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (process.env.CACHE_DEFAULT_TTL) {
|
|
64
|
+
pluginConfig.defaultTTL = parseInt(process.env.CACHE_DEFAULT_TTL) || pluginConfig.defaultTTL;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (process.env.CACHE_ENABLE_COMPRESSION) {
|
|
68
|
+
pluginConfig.enableCompression = process.env.CACHE_ENABLE_COMPRESSION === 'true';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (process.env.CACHEABLE_METHODS) {
|
|
72
|
+
pluginConfig.cacheableMethods = process.env.CACHEABLE_METHODS.split(',');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (process.env.CACHEABLE_STATUS_CODES) {
|
|
76
|
+
pluginConfig.cacheableStatusCodes = process.env.CACHEABLE_STATUS_CODES.split(',').map(Number);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Filtro para procesar solicitudes
|
|
82
|
+
* @param {Object} data - Datos de la solicitud
|
|
83
|
+
* @returns {Object} - Resultado del procesamiento
|
|
84
|
+
*/
|
|
85
|
+
async function requestProcessingFilter(data) {
|
|
86
|
+
const { req, res } = data;
|
|
87
|
+
|
|
88
|
+
// Verificar si la solicitud es cacheable
|
|
89
|
+
if (!isRequestCacheable(req)) {
|
|
90
|
+
return { allowed: true, cached: false };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generar clave de caché
|
|
94
|
+
const cacheKey = generateCacheKey(req);
|
|
95
|
+
|
|
96
|
+
// Verificar si está en caché
|
|
97
|
+
if (cacheStorage.has(cacheKey)) {
|
|
98
|
+
const cachedEntry = cacheStorage.get(cacheKey);
|
|
99
|
+
const metadata = cacheMetadata.get(cacheKey);
|
|
100
|
+
|
|
101
|
+
// Verificar si no ha expirado
|
|
102
|
+
if (Date.now() < metadata.expiryTime) {
|
|
103
|
+
// Enviar respuesta desde caché
|
|
104
|
+
res.writeHead(cachedEntry.statusCode, cachedEntry.headers);
|
|
105
|
+
res.end(cachedEntry.body);
|
|
106
|
+
|
|
107
|
+
console.log(`\x1b[32m[CACHE-HIT]\x1b[0m Solicitud ${req.method} ${req.url} servida desde caché`);
|
|
108
|
+
console.log(`\x1b[33m \x1b[0m → Status: ${cachedEntry.statusCode}, Size: ${cachedEntry.body.length} bytes, TTL restante: ${Math.floor((metadata.expiryTime - Date.now())/1000)}s`);
|
|
109
|
+
console.log(`\x1b[33m \x1b[0m → NO SE CONTACTÓ AL SERVIDOR BACKEND`);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
allowed: true,
|
|
113
|
+
cached: true,
|
|
114
|
+
cacheHit: true,
|
|
115
|
+
cacheKey
|
|
116
|
+
};
|
|
117
|
+
} else {
|
|
118
|
+
// Eliminar entrada expirada
|
|
119
|
+
cacheStorage.delete(cacheKey);
|
|
120
|
+
cacheMetadata.delete(cacheKey);
|
|
121
|
+
console.log(`\x1b[31m[CACHE-EXPIRED]\x1b[0m Entrada expirada eliminada para ${cacheKey}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// No está en caché, permitir que continúe
|
|
126
|
+
console.log(`\x1b[36m[CACHE-MISS]\x1b[0m Solicitud ${req.method} ${req.url} no está en caché`);
|
|
127
|
+
console.log(`\x1b[33m \x1b[0m → SE PROCESARÁ NORMALMENTE Y SE CONTACTARÁ AL SERVIDOR BACKEND`);
|
|
128
|
+
|
|
129
|
+
// Guardar la clave de caché en la solicitud para usarla más tarde
|
|
130
|
+
if (req) {
|
|
131
|
+
req.cacheKey = cacheKey;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
allowed: true,
|
|
136
|
+
cached: false,
|
|
137
|
+
cacheKey
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Hook para cuando se recibe una solicitud
|
|
143
|
+
* @param {Object} params - Parámetros del hook
|
|
144
|
+
*/
|
|
145
|
+
async function onRequestReceivedHook({ req, res }) {
|
|
146
|
+
console.log(`[CACHE-PLUGIN] Solicitud recibida: ${req.method} ${req.url}`);
|
|
147
|
+
|
|
148
|
+
// Podríamos hacer alguna lógica aquí si es necesario
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Hook para cuando se selecciona un servidor
|
|
153
|
+
* @param {Object} params - Parámetros del hook
|
|
154
|
+
*/
|
|
155
|
+
async function onServerSelectedHook({ req, res, server }) {
|
|
156
|
+
console.log(`[CACHE-PLUGIN] Servidor seleccionado: ${server.url} para ${req.method} ${req.url}`);
|
|
157
|
+
|
|
158
|
+
// Podríamos hacer alguna lógica aquí si es necesario
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Hook para cuando la respuesta está lista para ser enviada
|
|
163
|
+
* @param {Object} params - Parámetros del hook
|
|
164
|
+
*/
|
|
165
|
+
async function onResponseReadyHook({ req, res, serverRes, responseBody, server, responseTime }) {
|
|
166
|
+
console.log(`\x1b[35m[CACHE-STORE-PREPARING]\x1b[0m Respuesta lista para ${req.url}, tamaño: ${responseBody.length} bytes, código: ${serverRes.statusCode}`);
|
|
167
|
+
console.log(`\x1b[33m \x1b[0m → Proveniente del servidor: ${server.url}`);
|
|
168
|
+
|
|
169
|
+
// Verificar si la solicitud original tenía una clave de caché
|
|
170
|
+
if (req && req.cacheKey) {
|
|
171
|
+
// Verificar si la respuesta es cacheable
|
|
172
|
+
if (isResponseCacheable(req, serverRes)) {
|
|
173
|
+
// Crear entrada de caché
|
|
174
|
+
const cacheEntry = {
|
|
175
|
+
statusCode: serverRes.statusCode,
|
|
176
|
+
headers: { ...serverRes.headers },
|
|
177
|
+
body: responseBody
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Determinar TTL desde headers o usar valor por defecto
|
|
181
|
+
let ttl = pluginConfig.defaultTTL;
|
|
182
|
+
if (serverRes.headers['cache-control']) {
|
|
183
|
+
const cc = serverRes.headers['cache-control'];
|
|
184
|
+
const maxAgeMatch = cc.match(/max-age=(\d+)/);
|
|
185
|
+
if (maxAgeMatch) {
|
|
186
|
+
ttl = parseInt(maxAgeMatch[1]) * 1000; // Convertir a ms
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Crear metadatos
|
|
191
|
+
const metadata = {
|
|
192
|
+
expiryTime: Date.now() + ttl,
|
|
193
|
+
size: responseBody.length,
|
|
194
|
+
createdAt: Date.now(),
|
|
195
|
+
ttl: ttl
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Verificar límites de caché
|
|
199
|
+
if (cacheStorage.size >= pluginConfig.maxCacheSize) {
|
|
200
|
+
// Implementar LRU (eliminar la entrada menos recientemente usada)
|
|
201
|
+
evictLRUCacheEntry();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Almacenar en caché
|
|
205
|
+
cacheStorage.set(req.cacheKey, cacheEntry);
|
|
206
|
+
cacheMetadata.set(req.cacheKey, metadata);
|
|
207
|
+
|
|
208
|
+
console.log(`\x1b[32m[CACHE-STORED]\x1b[0m Almacenada respuesta para ${req.method} ${req.url}`);
|
|
209
|
+
console.log(`\x1b[33m \x1b[0m → TTL: ${ttl}ms, tamaño: ${responseBody.length} bytes`);
|
|
210
|
+
console.log(`\x1b[33m \x1b[0m → Ahora esta respuesta se servirá desde caché`);
|
|
211
|
+
} else {
|
|
212
|
+
console.log(`\x1b[31m[CACHE-SKIPPED]\x1b[0m La respuesta no es cacheable, omitiendo almacenamiento`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Hook para cuando se envía la respuesta
|
|
219
|
+
* @param {Object} params - Parámetros del hook
|
|
220
|
+
*/
|
|
221
|
+
async function onResponseSentHook({ req, res, serverRes, responseTime, server }) {
|
|
222
|
+
console.log(`[CACHE-PLUGIN] Respuesta enviada para ${req.url}, tiempo: ${responseTime}ms, código: ${serverRes.statusCode}`);
|
|
223
|
+
|
|
224
|
+
// Verificar si la solicitud original tenía una clave de caché
|
|
225
|
+
if (req && req.cacheKey) {
|
|
226
|
+
// Verificar si la respuesta es cacheable
|
|
227
|
+
if (isResponseCacheable(req, serverRes)) {
|
|
228
|
+
// Almacenar la respuesta en caché
|
|
229
|
+
storeResponseInCache(req, serverRes, req.cacheKey);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Hook para cuando ocurre un error con el servidor
|
|
236
|
+
* @param {Object} params - Parámetros del hook
|
|
237
|
+
*/
|
|
238
|
+
async function onServerErrorHook(params) {
|
|
239
|
+
// Extraer parámetros con valores por defecto para evitar errores
|
|
240
|
+
const { req, res, server, error, responseTime } = params || {};
|
|
241
|
+
|
|
242
|
+
// Verificar que req exista antes de continuar
|
|
243
|
+
if (!req) {
|
|
244
|
+
console.log(`[CACHE-PLUGIN] Error recibido sin solicitud válida`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const serverUrl = server ? server.url : 'desconocido';
|
|
249
|
+
const errorMessage = error ? error.message || error : 'desconocido';
|
|
250
|
+
|
|
251
|
+
console.log(`[CACHE-PLUGIN] Error con servidor ${serverUrl} para ${req.url}: ${errorMessage}`);
|
|
252
|
+
|
|
253
|
+
// Solo intentar servir desde caché si no se han enviado headers aún
|
|
254
|
+
if (res && !res.headersSent) {
|
|
255
|
+
const cacheKey = generateCacheKey(req);
|
|
256
|
+
|
|
257
|
+
if (cacheStorage.has(cacheKey)) {
|
|
258
|
+
const cachedEntry = cacheStorage.get(cacheKey);
|
|
259
|
+
const metadata = cacheMetadata.get(cacheKey);
|
|
260
|
+
|
|
261
|
+
// Verificar si no ha expirado o si está dentro de un periodo de grace period
|
|
262
|
+
// (por ejemplo, permitir servir contenido ligeramente expirado mientras se intenta reconectar)
|
|
263
|
+
const gracePeriod = 300000; // 5 minutos extra como grace period
|
|
264
|
+
if (Date.now() < metadata.expiryTime + gracePeriod) {
|
|
265
|
+
// Enviar respuesta desde caché como fallback
|
|
266
|
+
res.writeHead(cachedEntry.statusCode, cachedEntry.headers);
|
|
267
|
+
res.end(cachedEntry.body);
|
|
268
|
+
|
|
269
|
+
console.log(`\x1b[33m[CACHE-FALLBACK]\x1b[0m Solicitud ${req.method} ${req.url} servida desde caché como fallback`);
|
|
270
|
+
console.log(`\x1b[33m \x1b[0m → Servidor backend no disponible, usando versión en caché`);
|
|
271
|
+
|
|
272
|
+
return; // No continuar con el manejo de error estándar
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log(`\x1b[31m[CACHE-NONE-AVAILABLE]\x1b[0m No hay versión en caché disponible para ${req.url}, servidor no disponible`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Verifica si una solicitud es cacheable
|
|
282
|
+
* @param {Object} req - Objeto de solicitud
|
|
283
|
+
* @returns {boolean} - Verdadero si es cacheable
|
|
284
|
+
*/
|
|
285
|
+
function isRequestCacheable(req) {
|
|
286
|
+
// Verificar método HTTP
|
|
287
|
+
if (!pluginConfig.cacheableMethods.includes(req.method)) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Verificar si tiene encabezados que indican no cachear
|
|
292
|
+
if (req.headers && req.headers['cache-control'] && req.headers['cache-control'].includes('no-cache')) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (req.headers && req.headers['pragma'] && req.headers['pragma'].includes('no-cache')) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Verifica si una respuesta es cacheable
|
|
305
|
+
* @param {Object} req - Objeto de solicitud
|
|
306
|
+
* @param {Object} res - Objeto de respuesta del servidor backend
|
|
307
|
+
* @returns {boolean} - Verdadero si es cacheable
|
|
308
|
+
*/
|
|
309
|
+
function isResponseCacheable(req, res) {
|
|
310
|
+
// Verificar si la solicitud era cacheable
|
|
311
|
+
if (!isRequestCacheable(req)) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Verificar código de estado
|
|
316
|
+
if (!pluginConfig.cacheableStatusCodes.includes(res.statusCode)) {
|
|
317
|
+
console.log(`[CACHE-DEBUG] Código de estado ${res.statusCode} no es cacheable`);
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Verificar encabezados de respuesta
|
|
322
|
+
if (res.headers && res.headers['cache-control'] && res.headers['cache-control'].includes('no-cache')) {
|
|
323
|
+
console.log('[CACHE-DEBUG] Header cache-control indica no-cache');
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (res.headers && res.headers['pragma'] && res.headers['pragma'].includes('no-cache')) {
|
|
328
|
+
console.log('[CACHE-DEBUG] Header pragma indica no-cache');
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.log(`[CACHE-DEBUG] Respuesta es cacheable: statusCode=${res.statusCode}`);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Genera una clave única para la caché basada en la solicitud
|
|
338
|
+
* @param {Object} req - Objeto de solicitud
|
|
339
|
+
* @returns {string} - Clave de caché
|
|
340
|
+
*/
|
|
341
|
+
function generateCacheKey(req) {
|
|
342
|
+
// Crear una representación única de la solicitud
|
|
343
|
+
const keyData = {
|
|
344
|
+
method: req.method,
|
|
345
|
+
url: req.url,
|
|
346
|
+
headers: {
|
|
347
|
+
accept: req.headers.accept,
|
|
348
|
+
'accept-encoding': req.headers['accept-encoding'],
|
|
349
|
+
'user-agent': req.headers['user-agent']
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// Convertir a string y generar hash
|
|
354
|
+
const keyString = JSON.stringify(keyData);
|
|
355
|
+
return crypto.createHash('md5').update(keyString).digest('hex');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Almacena una respuesta en caché
|
|
360
|
+
* @param {Object} req - Objeto de solicitud
|
|
361
|
+
* @param {Object} serverRes - Objeto de respuesta del servidor backend
|
|
362
|
+
* @param {string} cacheKey - Clave de caché
|
|
363
|
+
*/
|
|
364
|
+
function storeResponseInCache(req, serverRes, cacheKey) {
|
|
365
|
+
// Debido a que no podemos interceptar directamente la respuesta en este punto,
|
|
366
|
+
// necesitamos una estrategia diferente. Vamos a usar el sistema de hooks
|
|
367
|
+
// del balanceador para interceptar la respuesta antes de que se envíe al cliente.
|
|
368
|
+
// Pero como no tenemos un hook específico para eso, vamos a implementar
|
|
369
|
+
// una solución alternativa registrando un middleware en el servidor proxy.
|
|
370
|
+
|
|
371
|
+
// Esta implementación requiere una modificación en el sistema del balanceador
|
|
372
|
+
// para permitir la interceptación de la respuesta, lo cual no es posible
|
|
373
|
+
// directamente desde un plugin sin modificar el código base.
|
|
374
|
+
|
|
375
|
+
// Por lo tanto, implementaremos una solución parcial que registra la intención
|
|
376
|
+
// de cachear la respuesta, y dejamos que el sistema principal maneje
|
|
377
|
+
// la captura de la respuesta.
|
|
378
|
+
|
|
379
|
+
console.log(`[CACHE-DEFERRED] Se detectó que la respuesta para ${req.method} ${req.url} debería ser cacheada`);
|
|
380
|
+
console.log(`[CACHE-DEFERRED] Clave de caché: ${cacheKey}`);
|
|
381
|
+
|
|
382
|
+
// En una implementación completa, aquí conectaríamos con un sistema
|
|
383
|
+
// que permite interceptar la respuesta del servidor backend antes
|
|
384
|
+
// de enviarla al cliente, pero eso requiere cambios en el código base.
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Elimina la entrada menos recientemente usada de la caché
|
|
389
|
+
*/
|
|
390
|
+
function evictLRUCacheEntry() {
|
|
391
|
+
let oldestTime = Infinity;
|
|
392
|
+
let oldestKey = null;
|
|
393
|
+
|
|
394
|
+
for (const [key, metadata] of cacheMetadata.entries()) {
|
|
395
|
+
if (metadata.createdAt < oldestTime) {
|
|
396
|
+
oldestTime = metadata.createdAt;
|
|
397
|
+
oldestKey = key;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (oldestKey) {
|
|
402
|
+
cacheStorage.delete(oldestKey);
|
|
403
|
+
cacheMetadata.delete(oldestKey);
|
|
404
|
+
console.log(`[CACHE-EVICTION] Entrada LRU eliminada: ${oldestKey}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Inicia la tarea de limpieza periódica de entradas expiradas
|
|
410
|
+
*/
|
|
411
|
+
function startCacheCleanupTask() {
|
|
412
|
+
// Limpiar entradas expiradas cada 5 minutos
|
|
413
|
+
setInterval(() => {
|
|
414
|
+
const initialSize = cacheStorage.size;
|
|
415
|
+
const now = Date.now();
|
|
416
|
+
|
|
417
|
+
for (const [key, metadata] of cacheMetadata.entries()) {
|
|
418
|
+
if (now >= metadata.expiryTime) {
|
|
419
|
+
cacheStorage.delete(key);
|
|
420
|
+
cacheMetadata.delete(key);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const removedCount = initialSize - cacheStorage.size;
|
|
425
|
+
if (removedCount > 0) {
|
|
426
|
+
console.log(`[CACHE-CLEANUP] Eliminadas ${removedCount} entradas expiradas. Tamaño actual: ${cacheStorage.size}`);
|
|
427
|
+
}
|
|
428
|
+
}, 300000); // 5 minutos
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Obtiene estadísticas de la caché
|
|
433
|
+
* @returns {Object} - Estadísticas de la caché
|
|
434
|
+
*/
|
|
435
|
+
function getCacheStats() {
|
|
436
|
+
return {
|
|
437
|
+
size: cacheStorage.size,
|
|
438
|
+
maxSize: pluginConfig.maxCacheSize,
|
|
439
|
+
utilization: (cacheStorage.size / pluginConfig.maxCacheSize) * 100,
|
|
440
|
+
entries: Array.from(cacheStorage.keys())
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Limpia toda la caché
|
|
446
|
+
*/
|
|
447
|
+
function clearCache() {
|
|
448
|
+
cacheStorage.clear();
|
|
449
|
+
cacheMetadata.clear();
|
|
450
|
+
console.log('[CACHE-CLEAR] Caché completamente limpiada');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Desinicializa el plugin
|
|
455
|
+
* @param {Object} balancer - Instancia del balanceador
|
|
456
|
+
*/
|
|
457
|
+
async function deinit(balancer) {
|
|
458
|
+
console.log('Desactivando plugin de cache robusto...');
|
|
459
|
+
|
|
460
|
+
// Limpiar recursos si es necesario
|
|
461
|
+
cacheStorage.clear();
|
|
462
|
+
cacheMetadata.clear();
|
|
463
|
+
|
|
464
|
+
console.log('Plugin de cache robusto desactivado correctamente');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Exportar funciones
|
|
468
|
+
module.exports = {
|
|
469
|
+
init,
|
|
470
|
+
deinit,
|
|
471
|
+
getCacheStats,
|
|
472
|
+
clearCache,
|
|
473
|
+
// Exportar también funciones útiles para otros módulos
|
|
474
|
+
isRequestCacheable,
|
|
475
|
+
isResponseCacheable,
|
|
476
|
+
generateCacheKey
|
|
477
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Plugin de Cache Robusto",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Plugin para implementar un sistema de cache robusto con hooks y filtros",
|
|
5
|
+
"author": "Sistema Kukuy",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"kukuyVersion": "^1.6.0",
|
|
8
|
+
"hooks": [
|
|
9
|
+
"onRequestReceived",
|
|
10
|
+
"onResponseSent",
|
|
11
|
+
"onServerSelected"
|
|
12
|
+
],
|
|
13
|
+
"filters": [
|
|
14
|
+
"request_processing"
|
|
15
|
+
],
|
|
16
|
+
"enabled": true
|
|
17
|
+
}
|
|
@@ -11,11 +11,13 @@ async function init(balancer) {
|
|
|
11
11
|
// Registrar un filtro de ejemplo
|
|
12
12
|
extension.registerFilter('request_processing', async (data) => {
|
|
13
13
|
const { req, res } = data;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if (req && res) {
|
|
15
|
+
console.log(`Plugin: Procesando solicitud ${req.method} ${req.url}`);
|
|
16
|
+
|
|
17
|
+
// Agregar un header personalizado
|
|
18
|
+
req.headers['x-plugin-example'] = 'activated';
|
|
19
|
+
}
|
|
20
|
+
|
|
19
21
|
return { allowed: true };
|
|
20
22
|
}, 5);
|
|
21
23
|
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"name": "Plugin de Ejemplo",
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"description": "Plugin de ejemplo para demostrar la arquitectura de plugins",
|
|
5
|
-
"author": "
|
|
5
|
+
"author": "kukuy",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"kukuyVersion": "^1.6.0",
|
|
8
8
|
"hooks": ["onRequestReceived", "onResponseSent"],
|
|
9
9
|
"filters": ["request_processing"],
|
|
10
|
-
"enabled":
|
|
10
|
+
"enabled": false
|
|
11
11
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const https = require('https');
|
|
3
|
+
const url = require('url');
|
|
4
|
+
|
|
5
|
+
let healthCheckIntervalId = null;
|
|
6
|
+
|
|
7
|
+
async function init(balancer) {
|
|
8
|
+
console.log('Inicializando plugin de verificación de salud periódica...');
|
|
9
|
+
|
|
10
|
+
const config = balancer.config;
|
|
11
|
+
const healthCheckInterval = config.healthCheckInterval || 30000; // 30 segundos por defecto
|
|
12
|
+
const serverPool = balancer.serverPool;
|
|
13
|
+
|
|
14
|
+
// Función para verificar la salud de un servidor
|
|
15
|
+
async function checkServerHealth(server) {
|
|
16
|
+
try {
|
|
17
|
+
const parsedUrl = url.parse(server.url);
|
|
18
|
+
const options = {
|
|
19
|
+
hostname: parsedUrl.hostname,
|
|
20
|
+
port: parsedUrl.port,
|
|
21
|
+
path: '/health', // Ruta estándar para verificación de salud
|
|
22
|
+
method: 'GET',
|
|
23
|
+
timeout: 5000, // 5 segundos de timeout
|
|
24
|
+
// Agregar headers para identificar la solicitud de health check
|
|
25
|
+
headers: {
|
|
26
|
+
'User-Agent': 'Kukuy-Health-Check/1.0'
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Emitir hook antes de realizar el check
|
|
31
|
+
await executeHook(balancer, 'onHealthCheckStart', { server, checkTime: new Date() });
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const request = server.protocol === 'https:'
|
|
35
|
+
? https.request(options)
|
|
36
|
+
: http.request(options);
|
|
37
|
+
|
|
38
|
+
request.on('response', (res) => {
|
|
39
|
+
// Consumir el cuerpo de la respuesta para liberar recursos
|
|
40
|
+
res.resume();
|
|
41
|
+
|
|
42
|
+
// Considerar saludable si obtenemos una respuesta exitosa
|
|
43
|
+
const isHealthy = res.statusCode >= 200 && res.statusCode < 400;
|
|
44
|
+
|
|
45
|
+
// Actualizar estado del servidor
|
|
46
|
+
server.healthy = isHealthy;
|
|
47
|
+
server.lastChecked = Date.now();
|
|
48
|
+
|
|
49
|
+
// Emitir hook después de completar el check
|
|
50
|
+
executeHook(balancer, 'onHealthCheckComplete', {
|
|
51
|
+
server,
|
|
52
|
+
isHealthy,
|
|
53
|
+
statusCode: res.statusCode,
|
|
54
|
+
checkTime: new Date()
|
|
55
|
+
}).then(() => {
|
|
56
|
+
// Resolver la promesa después de emitir el hook
|
|
57
|
+
resolve(isHealthy);
|
|
58
|
+
}).catch(() => {
|
|
59
|
+
// En caso de error al emitir el hook, resolver igualmente
|
|
60
|
+
resolve(isHealthy);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
request.on('error', (err) => {
|
|
65
|
+
// Actualizar estado del servidor
|
|
66
|
+
server.healthy = false;
|
|
67
|
+
server.lastChecked = Date.now();
|
|
68
|
+
|
|
69
|
+
// Emitir hook cuando ocurre un error en el check
|
|
70
|
+
executeHook(balancer, 'onHealthCheckError', {
|
|
71
|
+
server,
|
|
72
|
+
error: err,
|
|
73
|
+
checkTime: new Date()
|
|
74
|
+
}).then(() => {
|
|
75
|
+
resolve(false);
|
|
76
|
+
}).catch(() => {
|
|
77
|
+
resolve(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
request.on('timeout', () => {
|
|
82
|
+
// Actualizar estado del servidor
|
|
83
|
+
server.healthy = false;
|
|
84
|
+
server.lastChecked = Date.now();
|
|
85
|
+
|
|
86
|
+
// Emitir hook cuando ocurre un timeout en el check
|
|
87
|
+
executeHook(balancer, 'onHealthCheckTimeout', {
|
|
88
|
+
server,
|
|
89
|
+
checkTime: new Date()
|
|
90
|
+
}).then(() => {
|
|
91
|
+
resolve(false);
|
|
92
|
+
}).catch(() => {
|
|
93
|
+
resolve(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
request.end();
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Emitir hook cuando ocurre una excepción
|
|
101
|
+
await executeHook(balancer, 'onHealthCheckException', {
|
|
102
|
+
server,
|
|
103
|
+
error,
|
|
104
|
+
checkTime: new Date()
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Actualizar estado del servidor
|
|
108
|
+
server.healthy = false;
|
|
109
|
+
server.lastChecked = Date.now();
|
|
110
|
+
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Función para verificar la salud de todos los servidores
|
|
116
|
+
async function checkAllServersHealth() {
|
|
117
|
+
console.log('[HEALTH-CHECKER] Iniciando verificación de salud periódica...');
|
|
118
|
+
|
|
119
|
+
const servers = serverPool.getServers();
|
|
120
|
+
|
|
121
|
+
for (const server of servers) {
|
|
122
|
+
console.log(`[HEALTH-CHECKER] Verificando salud de ${server.url}...`);
|
|
123
|
+
await checkServerHealth(server);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Invalidar la caché de servidores saludables después de la verificación
|
|
127
|
+
serverPool.invalidateCache();
|
|
128
|
+
|
|
129
|
+
console.log('[HEALTH-CHECKER] Verificación de salud periódica completada.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Iniciar verificación de salud periódica
|
|
133
|
+
healthCheckIntervalId = setInterval(checkAllServersHealth, healthCheckInterval);
|
|
134
|
+
|
|
135
|
+
console.log(`[HEALTH-CHECKER] Verificación de salud periódica iniciada. Intervalo: ${healthCheckInterval}ms`);
|
|
136
|
+
|
|
137
|
+
// Realizar una verificación inmediata
|
|
138
|
+
await checkAllServersHealth();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function deinit(balancer) {
|
|
142
|
+
console.log('Desactivando plugin de verificación de salud periódica...');
|
|
143
|
+
|
|
144
|
+
// Detener la verificación de salud periódica
|
|
145
|
+
if (healthCheckIntervalId) {
|
|
146
|
+
clearInterval(healthCheckIntervalId);
|
|
147
|
+
healthCheckIntervalId = null;
|
|
148
|
+
console.log('[HEALTH-CHECKER] Verificación de salud periódica detenida.');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Execute a hook if the balancer reference is available
|
|
154
|
+
* @param {Object} balancer - Reference to the balancer
|
|
155
|
+
* @param {string} hookName - Name of the hook to execute
|
|
156
|
+
* @param {Object} data - Data to pass to the hook
|
|
157
|
+
*/
|
|
158
|
+
async function executeHook(balancer, hookName, data) {
|
|
159
|
+
if (balancer && balancer.hookManager) {
|
|
160
|
+
try {
|
|
161
|
+
await balancer.hookManager.executeHooks(hookName, data);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(`Error executing ${hookName} hook:`, error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { init, deinit };
|