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,443 @@
|
|
|
1
|
+
// packages/cluster/src/middleware/graceful-shutdown.js
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Graceful Shutdown Middleware - Handles graceful shutdown of the application
|
|
6
|
+
* Provides endpoints to trigger graceful shutdown and monitors shutdown process
|
|
7
|
+
*/
|
|
8
|
+
class GracefulShutdownMiddleware extends EventEmitter {
|
|
9
|
+
constructor(clusterManager, options = {}) {
|
|
10
|
+
super();
|
|
11
|
+
|
|
12
|
+
this.clusterManager = clusterManager;
|
|
13
|
+
this.options = {
|
|
14
|
+
path: options.path || '/cluster/shutdown',
|
|
15
|
+
auth: options.auth || null,
|
|
16
|
+
timeout: options.timeout || 10000, // 10 seconds
|
|
17
|
+
forceTimeout: options.forceTimeout || 30000, // 30 seconds
|
|
18
|
+
...options
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
this.isShuttingDown = false;
|
|
22
|
+
this.shutdownStartTime = null;
|
|
23
|
+
this.shutdownTimeout = null;
|
|
24
|
+
this.forceShutdownTimeout = null;
|
|
25
|
+
this.connections = new Set();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get middleware function for HTTP framework integration
|
|
30
|
+
* @returns {Function} Middleware function
|
|
31
|
+
*/
|
|
32
|
+
middleware() {
|
|
33
|
+
return async (ctx, next) => {
|
|
34
|
+
// Handle shutdown endpoint
|
|
35
|
+
if (ctx.path === this.options.path && ctx.method === 'POST') {
|
|
36
|
+
await this.handleShutdownRequest(ctx);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Handle status endpoint
|
|
41
|
+
if (ctx.path === `${this.options.path}/status` && ctx.method === 'GET') {
|
|
42
|
+
await this.handleShutdownStatus(ctx);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Track active connections during shutdown
|
|
47
|
+
if (this.isShuttingDown) {
|
|
48
|
+
ctx.status = 503;
|
|
49
|
+
ctx.set('Retry-After', '60');
|
|
50
|
+
ctx.body = {
|
|
51
|
+
status: 'error',
|
|
52
|
+
message: 'Service is shutting down',
|
|
53
|
+
timestamp: new Date().toISOString()
|
|
54
|
+
};
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Track connection
|
|
59
|
+
const connection = { ctx, startTime: Date.now() };
|
|
60
|
+
this.connections.add(connection);
|
|
61
|
+
|
|
62
|
+
// Remove connection when response is finished
|
|
63
|
+
ctx.res.on('finish', () => {
|
|
64
|
+
this.connections.delete(connection);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await next();
|
|
69
|
+
} finally {
|
|
70
|
+
this.connections.delete(connection);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Handle shutdown request
|
|
77
|
+
* @param {Object} ctx - Context object
|
|
78
|
+
*/
|
|
79
|
+
async handleShutdownRequest(ctx) {
|
|
80
|
+
// Check authentication if required
|
|
81
|
+
if (this.options.auth && !this.checkAuth(ctx)) {
|
|
82
|
+
ctx.status = 401;
|
|
83
|
+
ctx.body = {
|
|
84
|
+
status: 'error',
|
|
85
|
+
message: 'Unauthorized',
|
|
86
|
+
timestamp: new Date().toISOString()
|
|
87
|
+
};
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if already shutting down
|
|
92
|
+
if (this.isShuttingDown) {
|
|
93
|
+
ctx.status = 409;
|
|
94
|
+
ctx.body = {
|
|
95
|
+
status: 'error',
|
|
96
|
+
message: 'Shutdown already in progress',
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
shutdownStartTime: this.shutdownStartTime
|
|
99
|
+
};
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Parse shutdown options
|
|
104
|
+
const { force = false, timeout = this.options.timeout, message = 'Shutdown requested' } = ctx.request.body || {};
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Start graceful shutdown
|
|
108
|
+
await this.startGracefulShutdown({
|
|
109
|
+
force,
|
|
110
|
+
timeout: parseInt(timeout, 10),
|
|
111
|
+
message
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
ctx.status = 202;
|
|
115
|
+
ctx.body = {
|
|
116
|
+
status: 'accepted',
|
|
117
|
+
message: 'Graceful shutdown initiated',
|
|
118
|
+
timestamp: new Date().toISOString(),
|
|
119
|
+
shutdownStartTime: this.shutdownStartTime,
|
|
120
|
+
timeout: this.options.timeout,
|
|
121
|
+
forceTimeout: this.options.forceTimeout,
|
|
122
|
+
activeConnections: this.connections.size
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
} catch (error) {
|
|
126
|
+
ctx.status = 500;
|
|
127
|
+
ctx.body = {
|
|
128
|
+
status: 'error',
|
|
129
|
+
message: 'Failed to initiate shutdown',
|
|
130
|
+
timestamp: new Date().toISOString(),
|
|
131
|
+
error: error.message
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Handle shutdown status request
|
|
138
|
+
* @param {Object} ctx - Context object
|
|
139
|
+
*/
|
|
140
|
+
async handleShutdownStatus(ctx) {
|
|
141
|
+
ctx.status = 200;
|
|
142
|
+
ctx.body = {
|
|
143
|
+
status: 'success',
|
|
144
|
+
timestamp: new Date().toISOString(),
|
|
145
|
+
shutdown: {
|
|
146
|
+
isShuttingDown: this.isShuttingDown,
|
|
147
|
+
startTime: this.shutdownStartTime,
|
|
148
|
+
elapsedTime: this.shutdownStartTime ? Date.now() - this.shutdownStartTime : 0,
|
|
149
|
+
activeConnections: this.connections.size,
|
|
150
|
+
timeout: this.options.timeout,
|
|
151
|
+
forceTimeout: this.options.forceTimeout
|
|
152
|
+
},
|
|
153
|
+
cluster: this.clusterManager ? {
|
|
154
|
+
totalWorkers: this.clusterManager.workers ? this.clusterManager.workers.size : 0,
|
|
155
|
+
activeWorkers: this.clusterManager.workers ?
|
|
156
|
+
Array.from(this.clusterManager.workers.values()).filter(w => w.state === 'online').length : 0
|
|
157
|
+
} : null
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Start graceful shutdown process
|
|
163
|
+
* @param {Object} options - Shutdown options
|
|
164
|
+
*/
|
|
165
|
+
async startGracefulShutdown(options = {}) {
|
|
166
|
+
if (this.isShuttingDown) {
|
|
167
|
+
throw new Error('Shutdown already in progress');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.isShuttingDown = true;
|
|
171
|
+
this.shutdownStartTime = Date.now();
|
|
172
|
+
|
|
173
|
+
console.log(`🚨 Starting graceful shutdown: ${options.message}`);
|
|
174
|
+
this.emit('shutdown:started', {
|
|
175
|
+
timestamp: this.shutdownStartTime,
|
|
176
|
+
options
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Set shutdown timeout
|
|
180
|
+
this.shutdownTimeout = setTimeout(() => {
|
|
181
|
+
this.handleShutdownTimeout();
|
|
182
|
+
}, options.timeout || this.options.timeout);
|
|
183
|
+
|
|
184
|
+
// Set force shutdown timeout
|
|
185
|
+
this.forceShutdownTimeout = setTimeout(() => {
|
|
186
|
+
this.handleForceShutdown();
|
|
187
|
+
}, options.force ? 0 : this.options.forceTimeout);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
// Notify cluster manager
|
|
191
|
+
if (this.clusterManager) {
|
|
192
|
+
await this.notifyClusterManager(options);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Wait for active connections to complete
|
|
196
|
+
await this.waitForConnections();
|
|
197
|
+
|
|
198
|
+
// Perform cleanup
|
|
199
|
+
await this.performCleanup();
|
|
200
|
+
|
|
201
|
+
// Complete shutdown
|
|
202
|
+
await this.completeShutdown();
|
|
203
|
+
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('Graceful shutdown error:', error);
|
|
206
|
+
this.emit('shutdown:error', { error });
|
|
207
|
+
|
|
208
|
+
// Force shutdown on error
|
|
209
|
+
this.handleForceShutdown();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Notify cluster manager about shutdown
|
|
215
|
+
* @param {Object} options - Shutdown options
|
|
216
|
+
*/
|
|
217
|
+
async notifyClusterManager(options) {
|
|
218
|
+
if (!this.clusterManager) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log('📤 Notifying cluster manager about shutdown...');
|
|
223
|
+
|
|
224
|
+
// Send shutdown signal to all workers
|
|
225
|
+
this.clusterManager.workers.forEach((info, pid) => {
|
|
226
|
+
try {
|
|
227
|
+
info.worker.send({ type: 'SHUTDOWN', reason: options.message });
|
|
228
|
+
console.log(`📤 Sent shutdown signal to worker ${pid}`);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error(`Failed to send shutdown signal to worker ${pid}:`, error);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
this.emit('shutdown:notified', { workers: this.clusterManager.workers.size });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Wait for active connections to complete
|
|
239
|
+
* @returns {Promise<void>}
|
|
240
|
+
*/
|
|
241
|
+
async waitForConnections() {
|
|
242
|
+
if (this.connections.size === 0) {
|
|
243
|
+
console.log('✅ No active connections to wait for');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log(`⏳ Waiting for ${this.connections.size} active connections to complete...`);
|
|
248
|
+
this.emit('shutdown:waiting', { connections: this.connections.size });
|
|
249
|
+
|
|
250
|
+
// Wait for all connections to complete or timeout
|
|
251
|
+
const startTime = Date.now();
|
|
252
|
+
const maxWaitTime = this.options.timeout;
|
|
253
|
+
|
|
254
|
+
while (this.connections.size > 0 && Date.now() - startTime < maxWaitTime) {
|
|
255
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (this.connections.size > 0) {
|
|
259
|
+
console.log(`⚠️ ${this.connections.size} connections still active after timeout`);
|
|
260
|
+
this.emit('shutdown:connectionsTimeout', { remaining: this.connections.size });
|
|
261
|
+
} else {
|
|
262
|
+
console.log('✅ All connections completed');
|
|
263
|
+
this.emit('shutdown:connectionsComplete');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Perform cleanup tasks
|
|
269
|
+
* @returns {Promise<void>}
|
|
270
|
+
*/
|
|
271
|
+
async performCleanup() {
|
|
272
|
+
console.log('🧹 Performing cleanup tasks...');
|
|
273
|
+
this.emit('shutdown:cleanupStarted');
|
|
274
|
+
|
|
275
|
+
// Close database connections, file handles, etc.
|
|
276
|
+
// This is where you would add your application-specific cleanup logic
|
|
277
|
+
|
|
278
|
+
// Example: Close database connections
|
|
279
|
+
// if (this.clusterManager.db) {
|
|
280
|
+
// await this.clusterManager.db.close();
|
|
281
|
+
// }
|
|
282
|
+
|
|
283
|
+
// Example: Close file handles
|
|
284
|
+
// if (this.clusterManager.fileHandles) {
|
|
285
|
+
// this.clusterManager.fileHandles.forEach(handle => handle.close());
|
|
286
|
+
// }
|
|
287
|
+
|
|
288
|
+
console.log('✅ Cleanup completed');
|
|
289
|
+
this.emit('shutdown:cleanupCompleted');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Complete shutdown process
|
|
294
|
+
* @returns {Promise<void>}
|
|
295
|
+
*/
|
|
296
|
+
async completeShutdown() {
|
|
297
|
+
console.log('✅ Graceful shutdown completed');
|
|
298
|
+
this.emit('shutdown:completed');
|
|
299
|
+
|
|
300
|
+
// Clear timeouts
|
|
301
|
+
if (this.shutdownTimeout) {
|
|
302
|
+
clearTimeout(this.shutdownTimeout);
|
|
303
|
+
this.shutdownTimeout = null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (this.forceShutdownTimeout) {
|
|
307
|
+
clearTimeout(this.forceShutdownTimeout);
|
|
308
|
+
this.forceShutdownTimeout = null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Exit process
|
|
312
|
+
setTimeout(() => {
|
|
313
|
+
console.log('👋 Process exiting');
|
|
314
|
+
process.exit(0);
|
|
315
|
+
}, 1000);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Handle shutdown timeout
|
|
320
|
+
*/
|
|
321
|
+
handleShutdownTimeout() {
|
|
322
|
+
console.log('⏰ Shutdown timeout reached, forcing shutdown');
|
|
323
|
+
this.emit('shutdown:timeout');
|
|
324
|
+
|
|
325
|
+
this.handleForceShutdown();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Handle force shutdown
|
|
330
|
+
*/
|
|
331
|
+
handleForceShutdown() {
|
|
332
|
+
console.log('💥 Force shutdown initiated');
|
|
333
|
+
this.emit('shutdown:force');
|
|
334
|
+
|
|
335
|
+
// Clear all timeouts
|
|
336
|
+
if (this.shutdownTimeout) {
|
|
337
|
+
clearTimeout(this.shutdownTimeout);
|
|
338
|
+
this.shutdownTimeout = null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (this.forceShutdownTimeout) {
|
|
342
|
+
clearTimeout(this.forceShutdownTimeout);
|
|
343
|
+
this.forceShutdownTimeout = null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Force exit
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
console.log('💥 Force exiting process');
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}, 1000);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Check authentication
|
|
355
|
+
* @param {Object} ctx - Context object
|
|
356
|
+
* @returns {boolean} Authentication status
|
|
357
|
+
*/
|
|
358
|
+
checkAuth(ctx) {
|
|
359
|
+
if (typeof this.options.auth === 'function') {
|
|
360
|
+
return this.options.auth(ctx);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (typeof this.options.auth === 'object') {
|
|
364
|
+
// Check API key
|
|
365
|
+
if (this.options.auth.apiKey) {
|
|
366
|
+
const apiKey = ctx.headers['x-api-key'] || ctx.query.apiKey;
|
|
367
|
+
return apiKey === this.options.auth.apiKey;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Check basic auth
|
|
371
|
+
if (this.options.auth.username && this.options.auth.password) {
|
|
372
|
+
const authHeader = ctx.headers.authorization;
|
|
373
|
+
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const credentials = Buffer.from(authHeader.slice(6), 'base64').toString();
|
|
378
|
+
const [username, password] = credentials.split(':');
|
|
379
|
+
return username === this.options.auth.username && password === this.options.auth.password;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return true; // No auth required
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get shutdown status
|
|
388
|
+
* @returns {Object} Shutdown status
|
|
389
|
+
*/
|
|
390
|
+
getStatus() {
|
|
391
|
+
return {
|
|
392
|
+
isShuttingDown: this.isShuttingDown,
|
|
393
|
+
startTime: this.shutdownStartTime,
|
|
394
|
+
elapsedTime: this.shutdownStartTime ? Date.now() - this.shutdownStartTime : 0,
|
|
395
|
+
activeConnections: this.connections.size,
|
|
396
|
+
timeout: this.options.timeout,
|
|
397
|
+
forceTimeout: this.options.forceTimeout
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Cancel shutdown
|
|
403
|
+
* @returns {boolean} Success status
|
|
404
|
+
*/
|
|
405
|
+
cancelShutdown() {
|
|
406
|
+
if (!this.isShuttingDown) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
console.log('🔄 Cancelling shutdown');
|
|
411
|
+
|
|
412
|
+
this.isShuttingDown = false;
|
|
413
|
+
this.shutdownStartTime = null;
|
|
414
|
+
|
|
415
|
+
// Clear timeouts
|
|
416
|
+
if (this.shutdownTimeout) {
|
|
417
|
+
clearTimeout(this.shutdownTimeout);
|
|
418
|
+
this.shutdownTimeout = null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (this.forceShutdownTimeout) {
|
|
422
|
+
clearTimeout(this.forceShutdownTimeout);
|
|
423
|
+
this.forceShutdownTimeout = null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
this.emit('shutdown:cancelled');
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Get middleware configuration
|
|
432
|
+
* @returns {Object} Middleware configuration
|
|
433
|
+
*/
|
|
434
|
+
getConfig() {
|
|
435
|
+
return {
|
|
436
|
+
...this.options,
|
|
437
|
+
isShuttingDown: this.isShuttingDown,
|
|
438
|
+
activeConnections: this.connections.size
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export default GracefulShutdownMiddleware;
|