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.
@@ -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;