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,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;