aetherframework-cluster 1.0.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/.env.example +90 -0
- package/README.md +1049 -0
- package/index.js +288 -0
- package/package.json +41 -0
- package/src/core/ClusterManager.js +109 -0
- package/src/core/HealthMonitor.js +571 -0
- package/src/core/LoadBalancer.js +531 -0
- package/src/core/WorkerManager.js +619 -0
- package/src/examples/advanced-cluster.js +150 -0
- package/src/examples/basic-cluster.js +107 -0
- package/src/examples/benchmark-cluster.js +112 -0
- package/src/examples/simple-app.js +52 -0
- package/src/middleware/cluster-health.js +330 -0
- package/src/middleware/graceful-shutdown.js +443 -0
- package/src/middleware/process-monitor.js +925 -0
- package/src/middleware/worker-stats.js +879 -0
- package/src/utils/cpu-detector.js +78 -0
- package/src/utils/env-loader.js +140 -0
- package/src/utils/signal-handler.js +90 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
// packages/cluster/src/core/LoadBalancer.js
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Load Balancer - Distributes requests across worker processes
|
|
6
|
+
* Supports multiple load balancing algorithms: round-robin, least-connections, ip-hash, weighted
|
|
7
|
+
*/
|
|
8
|
+
class LoadBalancer extends EventEmitter {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
super();
|
|
11
|
+
|
|
12
|
+
this.options = {
|
|
13
|
+
algorithm: options.algorithm || 'round-robin', // round-robin, least-connections, ip-hash, weighted
|
|
14
|
+
stickySessions: options.stickySessions || false,
|
|
15
|
+
sessionTimeout: options.sessionTimeout || 30000, // 30 seconds
|
|
16
|
+
maxConnectionsPerWorker: options.maxConnectionsPerWorker || 1000,
|
|
17
|
+
healthCheckInterval: options.healthCheckInterval || 10000, // 10 seconds
|
|
18
|
+
...options
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
this.workers = new Map();
|
|
22
|
+
this.connections = new Map();
|
|
23
|
+
this.currentIndex = 0;
|
|
24
|
+
this.sessionMap = new Map();
|
|
25
|
+
this.healthCheckInterval = null;
|
|
26
|
+
this.workerWeights = new Map();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add a worker to the load balancer
|
|
31
|
+
* @param {number} pid - Process ID
|
|
32
|
+
* @param {Object} options - Worker options
|
|
33
|
+
* @returns {Object} Worker object
|
|
34
|
+
*/
|
|
35
|
+
addWorker(pid, options = {}) {
|
|
36
|
+
const worker = {
|
|
37
|
+
pid,
|
|
38
|
+
weight: options.weight || 1,
|
|
39
|
+
connections: 0,
|
|
40
|
+
isHealthy: true,
|
|
41
|
+
lastHealthCheck: Date.now(),
|
|
42
|
+
addedAt: Date.now(),
|
|
43
|
+
metadata: options.metadata || {}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.workers.set(pid, worker);
|
|
47
|
+
this.connections.set(pid, 0);
|
|
48
|
+
this.workerWeights.set(pid, worker.weight);
|
|
49
|
+
|
|
50
|
+
console.log(`✅ Worker ${pid} added to load balancer`);
|
|
51
|
+
this.emit('worker:added', { pid, worker });
|
|
52
|
+
|
|
53
|
+
// Start health checks
|
|
54
|
+
this.startHealthChecks();
|
|
55
|
+
|
|
56
|
+
return worker;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Remove a worker from the load balancer
|
|
61
|
+
* @param {number} pid - Process ID
|
|
62
|
+
* @returns {boolean} Success status
|
|
63
|
+
*/
|
|
64
|
+
removeWorker(pid) {
|
|
65
|
+
const worker = this.workers.get(pid);
|
|
66
|
+
if (!worker) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.workers.delete(pid);
|
|
71
|
+
this.connections.delete(pid);
|
|
72
|
+
this.workerWeights.delete(pid);
|
|
73
|
+
|
|
74
|
+
// Clean up session mappings
|
|
75
|
+
for (const [sessionId, workerPid] of this.sessionMap.entries()) {
|
|
76
|
+
if (workerPid === pid) {
|
|
77
|
+
this.sessionMap.delete(sessionId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`🗑️ Worker ${pid} removed from load balancer`);
|
|
82
|
+
this.emit('worker:removed', { pid, worker });
|
|
83
|
+
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the next available worker for a request
|
|
89
|
+
* @param {string} sessionId - Session ID for sticky sessions
|
|
90
|
+
* @param {string} clientIp - Client IP for IP hash algorithm
|
|
91
|
+
* @returns {number|null} PID of the selected worker
|
|
92
|
+
*/
|
|
93
|
+
getNextWorker(sessionId = null, clientIp = null) {
|
|
94
|
+
// If sticky sessions enabled and session exists, return cached worker
|
|
95
|
+
if (sessionId && this.options.stickySessions) {
|
|
96
|
+
const cachedWorker = this.sessionMap.get(sessionId);
|
|
97
|
+
if (cachedWorker && this.isWorkerHealthy(cachedWorker)) {
|
|
98
|
+
this.incrementConnections(cachedWorker);
|
|
99
|
+
return cachedWorker;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const healthyWorkers = this.getHealthyWorkers();
|
|
104
|
+
|
|
105
|
+
if (healthyWorkers.length === 0) {
|
|
106
|
+
console.warn('⚠️ No healthy workers available');
|
|
107
|
+
this.emit('error:noHealthyWorkers');
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let selectedWorker;
|
|
112
|
+
|
|
113
|
+
switch (this.options.algorithm) {
|
|
114
|
+
case 'round-robin':
|
|
115
|
+
selectedWorker = this.roundRobin(healthyWorkers);
|
|
116
|
+
break;
|
|
117
|
+
case 'least-connections':
|
|
118
|
+
selectedWorker = this.leastConnections(healthyWorkers);
|
|
119
|
+
break;
|
|
120
|
+
case 'ip-hash':
|
|
121
|
+
selectedWorker = this.ipHash(healthyWorkers, clientIp);
|
|
122
|
+
break;
|
|
123
|
+
case 'weighted':
|
|
124
|
+
selectedWorker = this.weightedRoundRobin(healthyWorkers);
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
selectedWorker = this.roundRobin(healthyWorkers);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Cache session to worker mapping if sticky sessions enabled
|
|
131
|
+
if (sessionId && this.options.stickySessions) {
|
|
132
|
+
this.sessionMap.set(sessionId, selectedWorker);
|
|
133
|
+
|
|
134
|
+
// Set session expiration
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
this.sessionMap.delete(sessionId);
|
|
137
|
+
}, this.options.sessionTimeout);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Increment connection count
|
|
141
|
+
this.incrementConnections(selectedWorker);
|
|
142
|
+
|
|
143
|
+
this.emit('worker:selected', {
|
|
144
|
+
pid: selectedWorker,
|
|
145
|
+
algorithm: this.options.algorithm,
|
|
146
|
+
sessionId,
|
|
147
|
+
clientIp
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return selectedWorker;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Round-robin load balancing algorithm
|
|
155
|
+
* @param {Array} workers - Array of worker PIDs
|
|
156
|
+
* @returns {number} Selected worker PID
|
|
157
|
+
*/
|
|
158
|
+
roundRobin(workers) {
|
|
159
|
+
if (this.currentIndex >= workers.length) {
|
|
160
|
+
this.currentIndex = 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const worker = workers[this.currentIndex];
|
|
164
|
+
this.currentIndex++;
|
|
165
|
+
|
|
166
|
+
return worker;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Least connections load balancing algorithm
|
|
171
|
+
* @param {Array} workers - Array of worker PIDs
|
|
172
|
+
* @returns {number} Selected worker PID
|
|
173
|
+
*/
|
|
174
|
+
leastConnections(workers) {
|
|
175
|
+
let minConnections = Infinity;
|
|
176
|
+
let selectedWorker = null;
|
|
177
|
+
|
|
178
|
+
for (const workerPid of workers) {
|
|
179
|
+
const connections = this.connections.get(workerPid) || 0;
|
|
180
|
+
if (connections < minConnections) {
|
|
181
|
+
minConnections = connections;
|
|
182
|
+
selectedWorker = workerPid;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return selectedWorker;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* IP hash load balancing algorithm
|
|
191
|
+
* @param {Array} workers - Array of worker PIDs
|
|
192
|
+
* @param {string} ip - Client IP address
|
|
193
|
+
* @returns {number} Selected worker PID
|
|
194
|
+
*/
|
|
195
|
+
ipHash(workers, ip = '') {
|
|
196
|
+
if (!ip) {
|
|
197
|
+
return this.roundRobin(workers);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Simple IP hash algorithm
|
|
201
|
+
let hash = 0;
|
|
202
|
+
for (let i = 0; i < ip.length; i++) {
|
|
203
|
+
hash = ((hash << 5) - hash) + ip.charCodeAt(i);
|
|
204
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const index = Math.abs(hash) % workers.length;
|
|
208
|
+
return workers[index];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Weighted round-robin load balancing algorithm
|
|
213
|
+
* @param {Array} workers - Array of worker PIDs
|
|
214
|
+
* @returns {number} Selected worker PID
|
|
215
|
+
*/
|
|
216
|
+
weightedRoundRobin(workers) {
|
|
217
|
+
const weightedWorkers = [];
|
|
218
|
+
|
|
219
|
+
for (const workerPid of workers) {
|
|
220
|
+
const worker = this.workers.get(workerPid);
|
|
221
|
+
const weight = worker.weight || 1;
|
|
222
|
+
|
|
223
|
+
for (let i = 0; i < weight; i++) {
|
|
224
|
+
weightedWorkers.push(workerPid);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (this.currentIndex >= weightedWorkers.length) {
|
|
229
|
+
this.currentIndex = 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const worker = weightedWorkers[this.currentIndex];
|
|
233
|
+
this.currentIndex++;
|
|
234
|
+
|
|
235
|
+
return worker;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check if a worker is healthy
|
|
240
|
+
* @param {number} workerPid - Worker process ID
|
|
241
|
+
* @returns {boolean} Health status
|
|
242
|
+
*/
|
|
243
|
+
isWorkerHealthy(workerPid) {
|
|
244
|
+
const worker = this.workers.get(workerPid);
|
|
245
|
+
if (!worker) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check connection limit
|
|
250
|
+
const connections = this.connections.get(workerPid) || 0;
|
|
251
|
+
if (connections >= this.options.maxConnectionsPerWorker) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check health status
|
|
256
|
+
if (!worker.isHealthy) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check last health check time
|
|
261
|
+
const timeSinceLastCheck = Date.now() - worker.lastHealthCheck;
|
|
262
|
+
if (timeSinceLastCheck > this.options.healthCheckInterval * 2) {
|
|
263
|
+
return false; // No health check for 2 intervals, consider unhealthy
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get list of healthy workers
|
|
271
|
+
* @returns {Array} Array of healthy worker PIDs
|
|
272
|
+
*/
|
|
273
|
+
getHealthyWorkers() {
|
|
274
|
+
const healthyWorkers = [];
|
|
275
|
+
|
|
276
|
+
for (const [pid, worker] of this.workers.entries()) {
|
|
277
|
+
if (this.isWorkerHealthy(pid)) {
|
|
278
|
+
healthyWorkers.push(pid);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return healthyWorkers;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Increment connection count for a worker
|
|
287
|
+
* @param {number} workerPid - Worker process ID
|
|
288
|
+
*/
|
|
289
|
+
incrementConnections(workerPid) {
|
|
290
|
+
const current = this.connections.get(workerPid) || 0;
|
|
291
|
+
this.connections.set(workerPid, current + 1);
|
|
292
|
+
|
|
293
|
+
// Update worker connection count
|
|
294
|
+
const worker = this.workers.get(workerPid);
|
|
295
|
+
if (worker) {
|
|
296
|
+
worker.connections = current + 1;
|
|
297
|
+
worker.lastRequestTime = Date.now();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.emit('connection:incremented', { pid: workerPid, connections: current + 1 });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Decrement connection count for a worker
|
|
305
|
+
* @param {number} workerPid - Worker process ID
|
|
306
|
+
*/
|
|
307
|
+
decrementConnections(workerPid) {
|
|
308
|
+
const current = this.connections.get(workerPid) || 0;
|
|
309
|
+
if (current > 0) {
|
|
310
|
+
this.connections.set(workerPid, current - 1);
|
|
311
|
+
|
|
312
|
+
// Update worker connection count
|
|
313
|
+
const worker = this.workers.get(workerPid);
|
|
314
|
+
if (worker) {
|
|
315
|
+
worker.connections = current - 1;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this.emit('connection:decremented', { pid: workerPid, connections: Math.max(0, current - 1) });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Update worker health status
|
|
324
|
+
* @param {number} workerPid - Worker process ID
|
|
325
|
+
* @param {boolean} isHealthy - Health status
|
|
326
|
+
*/
|
|
327
|
+
updateWorkerHealth(workerPid, isHealthy) {
|
|
328
|
+
const worker = this.workers.get(workerPid);
|
|
329
|
+
if (worker) {
|
|
330
|
+
worker.isHealthy = isHealthy;
|
|
331
|
+
worker.lastHealthCheck = Date.now();
|
|
332
|
+
|
|
333
|
+
console.log(`Worker ${workerPid} health updated: ${isHealthy ? 'healthy' : 'unhealthy'}`);
|
|
334
|
+
this.emit('worker:healthUpdated', { pid: workerPid, isHealthy });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Start health checks for workers
|
|
340
|
+
*/
|
|
341
|
+
startHealthChecks() {
|
|
342
|
+
if (this.healthCheckInterval) {
|
|
343
|
+
clearInterval(this.healthCheckInterval);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.healthCheckInterval = setInterval(() => {
|
|
347
|
+
this.performHealthChecks();
|
|
348
|
+
}, this.options.healthCheckInterval);
|
|
349
|
+
|
|
350
|
+
console.log(`✅ Load balancer health checks started (interval: ${this.options.healthCheckInterval}ms)`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Perform health checks on all workers
|
|
355
|
+
*/
|
|
356
|
+
performHealthChecks() {
|
|
357
|
+
for (const [pid, worker] of this.workers.entries()) {
|
|
358
|
+
// Check connection count
|
|
359
|
+
const connections = this.connections.get(pid) || 0;
|
|
360
|
+
const isOverloaded = connections >= this.options.maxConnectionsPerWorker;
|
|
361
|
+
|
|
362
|
+
// Update health status
|
|
363
|
+
if (isOverloaded) {
|
|
364
|
+
this.updateWorkerHealth(pid, false);
|
|
365
|
+
this.emit('worker:overloaded', { pid, connections });
|
|
366
|
+
} else {
|
|
367
|
+
this.updateWorkerHealth(pid, true);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.emit('health:checkCompleted');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get load balancer statistics
|
|
376
|
+
* @returns {Object} Load balancer statistics
|
|
377
|
+
*/
|
|
378
|
+
getStats() {
|
|
379
|
+
const totalWorkers = this.workers.size;
|
|
380
|
+
const healthyWorkers = this.getHealthyWorkers().length;
|
|
381
|
+
const totalConnections = Array.from(this.connections.values()).reduce((sum, count) => sum + count, 0);
|
|
382
|
+
|
|
383
|
+
const workerStats = [];
|
|
384
|
+
for (const [pid, worker] of this.workers.entries()) {
|
|
385
|
+
const connections = this.connections.get(pid) || 0;
|
|
386
|
+
workerStats.push({
|
|
387
|
+
pid,
|
|
388
|
+
weight: worker.weight,
|
|
389
|
+
connections,
|
|
390
|
+
isHealthy: worker.isHealthy,
|
|
391
|
+
lastHealthCheck: worker.lastHealthCheck,
|
|
392
|
+
lastRequestTime: worker.lastRequestTime,
|
|
393
|
+
uptime: Date.now() - worker.addedAt,
|
|
394
|
+
metadata: worker.metadata
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
algorithm: this.options.algorithm,
|
|
400
|
+
stickySessions: this.options.stickySessions,
|
|
401
|
+
sessionTimeout: this.options.sessionTimeout,
|
|
402
|
+
maxConnectionsPerWorker: this.options.maxConnectionsPerWorker,
|
|
403
|
+
totalWorkers,
|
|
404
|
+
healthyWorkers,
|
|
405
|
+
unhealthyWorkers: totalWorkers - healthyWorkers,
|
|
406
|
+
totalConnections,
|
|
407
|
+
averageConnectionsPerWorker: totalWorkers > 0 ? totalConnections / totalWorkers : 0,
|
|
408
|
+
workers: workerStats,
|
|
409
|
+
sessionCount: this.sessionMap.size,
|
|
410
|
+
currentIndex: this.currentIndex,
|
|
411
|
+
timestamp: new Date().toISOString()
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Reset the load balancer
|
|
417
|
+
*/
|
|
418
|
+
reset() {
|
|
419
|
+
this.currentIndex = 0;
|
|
420
|
+
this.connections.clear();
|
|
421
|
+
this.sessionMap.clear();
|
|
422
|
+
|
|
423
|
+
// Reset all worker connection counts
|
|
424
|
+
for (const [pid, worker] of this.workers.entries()) {
|
|
425
|
+
worker.connections = 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
console.log('🔄 Load balancer reset');
|
|
429
|
+
this.emit('loadBalancer:reset');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Stop the load balancer
|
|
434
|
+
*/
|
|
435
|
+
stop() {
|
|
436
|
+
if (this.healthCheckInterval) {
|
|
437
|
+
clearInterval(this.healthCheckInterval);
|
|
438
|
+
this.healthCheckInterval = null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
console.log('🛑 Load balancer stopped');
|
|
442
|
+
this.emit('loadBalancer:stopped');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Update worker weight
|
|
447
|
+
* @param {number} pid - Worker process ID
|
|
448
|
+
* @param {number} weight - New weight
|
|
449
|
+
*/
|
|
450
|
+
updateWorkerWeight(pid, weight) {
|
|
451
|
+
const worker = this.workers.get(pid);
|
|
452
|
+
if (worker) {
|
|
453
|
+
worker.weight = Math.max(1, weight);
|
|
454
|
+
this.workerWeights.set(pid, worker.weight);
|
|
455
|
+
|
|
456
|
+
console.log(`Worker ${pid} weight updated to ${weight}`);
|
|
457
|
+
this.emit('worker:weightUpdated', { pid, weight });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Get worker by PID
|
|
463
|
+
* @param {number} pid - Worker process ID
|
|
464
|
+
* @returns {Object|null} Worker object
|
|
465
|
+
*/
|
|
466
|
+
getWorker(pid) {
|
|
467
|
+
const worker = this.workers.get(pid);
|
|
468
|
+
if (!worker) return null;
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
...worker,
|
|
472
|
+
connections: this.connections.get(pid) || 0,
|
|
473
|
+
isHealthy: this.isWorkerHealthy(pid)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get all workers
|
|
479
|
+
* @returns {Array} Array of worker objects
|
|
480
|
+
*/
|
|
481
|
+
getAllWorkers() {
|
|
482
|
+
const workers = [];
|
|
483
|
+
|
|
484
|
+
for (const [pid, worker] of this.workers.entries()) {
|
|
485
|
+
workers.push(this.getWorker(pid));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return workers;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Get session to worker mapping
|
|
493
|
+
* @param {string} sessionId - Session ID
|
|
494
|
+
* @returns {number|null} Worker PID
|
|
495
|
+
*/
|
|
496
|
+
getSessionWorker(sessionId) {
|
|
497
|
+
return this.sessionMap.get(sessionId) || null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Clear expired sessions
|
|
502
|
+
* @returns {number} Number of cleared sessions
|
|
503
|
+
*/
|
|
504
|
+
clearExpiredSessions() {
|
|
505
|
+
const now = Date.now();
|
|
506
|
+
let cleared = 0;
|
|
507
|
+
|
|
508
|
+
// Note: In a real implementation, we would track session creation time
|
|
509
|
+
// For now, we'll just clear old sessions periodically
|
|
510
|
+
const sessionCount = this.sessionMap.size;
|
|
511
|
+
if (sessionCount > 1000) {
|
|
512
|
+
// Clear 10% of oldest sessions if we have too many
|
|
513
|
+
const sessionsToClear = Math.floor(sessionCount * 0.1);
|
|
514
|
+
const keys = Array.from(this.sessionMap.keys()).slice(0, sessionsToClear);
|
|
515
|
+
|
|
516
|
+
for (const key of keys) {
|
|
517
|
+
this.sessionMap.delete(key);
|
|
518
|
+
cleared++;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (cleared > 0) {
|
|
523
|
+
console.log(`Cleared ${cleared} expired sessions`);
|
|
524
|
+
this.emit('sessions:cleared', { count: cleared });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return cleared;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export default LoadBalancer;
|