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,150 @@
|
|
|
1
|
+
// packages/cluster/src/examples/advanced-cluster.js
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import cluster from 'cluster';
|
|
6
|
+
import ClusterManager from '../core/ClusterManager.js';
|
|
7
|
+
import CPUDetector from '../utils/cpu-detector.js';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
function createApp() {
|
|
13
|
+
let requestCount = 0;
|
|
14
|
+
|
|
15
|
+
const server = http.createServer((req, res) => {
|
|
16
|
+
requestCount++;
|
|
17
|
+
|
|
18
|
+
console.log(`[Worker ${process.pid}] Received request #${requestCount}: ${req.method} ${req.url}`);
|
|
19
|
+
|
|
20
|
+
const responseData = {
|
|
21
|
+
status: 'ok',
|
|
22
|
+
pid: process.pid,
|
|
23
|
+
workerId: process.env.WORKER_ID || 'unknown',
|
|
24
|
+
requestCount: requestCount,
|
|
25
|
+
message: 'Hello from Cluster Worker',
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
url: req.url,
|
|
28
|
+
method: req.method
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
res.writeHead(200, {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'X-Worker-PID': process.pid,
|
|
34
|
+
'X-Worker-ID': process.env.WORKER_ID || 'unknown'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
res.end(JSON.stringify(responseData, null, 2));
|
|
38
|
+
|
|
39
|
+
if (process.send) {
|
|
40
|
+
process.send({ type: 'REQUEST_PROCESSED' });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 【Key fix】Use flag to avoid duplicate shutdown
|
|
45
|
+
let isShuttingDown = false;
|
|
46
|
+
|
|
47
|
+
const gracefulShutdown = () => {
|
|
48
|
+
if (isShuttingDown) return;
|
|
49
|
+
isShuttingDown = true;
|
|
50
|
+
|
|
51
|
+
console.log(`Worker ${process.pid} starting graceful shutdown...`);
|
|
52
|
+
|
|
53
|
+
server.close(() => {
|
|
54
|
+
console.log(`Worker ${process.pid} HTTP server closed`);
|
|
55
|
+
process.exit(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Set shutdown timeout
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
console.log(`Worker ${process.pid} shutdown timeout, forcing exit`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}, 5000);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Listen for shutdown messages from master process
|
|
66
|
+
process.on('message', (message) => {
|
|
67
|
+
if (message.type === 'SHUTDOWN') {
|
|
68
|
+
console.log(`Worker ${process.pid} received shutdown signal from master`);
|
|
69
|
+
gracefulShutdown();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Listen for system signals (as backup)
|
|
74
|
+
process.once('SIGTERM', () => {
|
|
75
|
+
console.log(`Worker ${process.pid} received SIGTERM`);
|
|
76
|
+
gracefulShutdown();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
process.once('SIGINT', () => {
|
|
80
|
+
console.log(`Worker ${process.pid} received SIGINT`);
|
|
81
|
+
gracefulShutdown();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
handleRequest: async (requestData, responseCallback) => {
|
|
86
|
+
const result = {
|
|
87
|
+
pid: process.pid,
|
|
88
|
+
workerId: process.env.WORKER_ID,
|
|
89
|
+
data: requestData,
|
|
90
|
+
processedAt: new Date().toISOString()
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (responseCallback) {
|
|
94
|
+
responseCallback(result);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
getStats: () => ({
|
|
101
|
+
pid: process.pid,
|
|
102
|
+
requestCount: requestCount,
|
|
103
|
+
uptime: process.uptime(),
|
|
104
|
+
memoryUsage: process.memoryUsage()
|
|
105
|
+
}),
|
|
106
|
+
|
|
107
|
+
close: (callback) => {
|
|
108
|
+
server.close(callback);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function main() {
|
|
114
|
+
console.log('🔧 Initializing Advanced Cluster Configuration...');
|
|
115
|
+
|
|
116
|
+
const cpuInfo = CPUDetector.getInfo();
|
|
117
|
+
const optimalWorkers = CPUDetector.getOptimalWorkerCount({ reserve: 1 });
|
|
118
|
+
|
|
119
|
+
console.log(`🖥️ Detected ${cpuInfo.count} cores. Using ${optimalWorkers} workers`);
|
|
120
|
+
|
|
121
|
+
const clusterManager = new ClusterManager({
|
|
122
|
+
workers: optimalWorkers,
|
|
123
|
+
port: 3000,
|
|
124
|
+
gracefulShutdown: true,
|
|
125
|
+
restartOnExit: true,
|
|
126
|
+
restartDelay: 1000,
|
|
127
|
+
shutdownTimeout: 10000
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Note: No longer listening for events here, as ClusterManager handles logging internally
|
|
131
|
+
// If custom event handling is needed, add it here but ensure no duplication
|
|
132
|
+
|
|
133
|
+
// Start the cluster
|
|
134
|
+
await clusterManager.start(createApp);
|
|
135
|
+
|
|
136
|
+
console.log('✅ Cluster is running. Press Ctrl+C to stop.');
|
|
137
|
+
|
|
138
|
+
// Note: No longer setting SignalHandler here, as ClusterManager handles signals internally
|
|
139
|
+
// Avoid signal handling conflicts
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Execute only in master process
|
|
143
|
+
if (cluster.isPrimary) {
|
|
144
|
+
main().catch((err) => {
|
|
145
|
+
console.error('Failed to start cluster:', err);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
// Worker processes don't execute any code, handled by ClusterManager.startWorker()
|
|
150
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// packages/cluster/examples/basic-cluster-simple.js
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import cluster from 'cluster';
|
|
4
|
+
import ClusterManager from '../core/ClusterManager.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Simple application factory function
|
|
8
|
+
*/
|
|
9
|
+
function createSimpleApp() {
|
|
10
|
+
const server = http.createServer((req, res) => {
|
|
11
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
12
|
+
res.end(JSON.stringify({
|
|
13
|
+
pid: process.pid,
|
|
14
|
+
message: 'Hello from Simple Cluster',
|
|
15
|
+
timestamp: new Date().toISOString()
|
|
16
|
+
}));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// 【Key fix】Use flag to avoid duplicate shutdown, consistent with advanced-cluster.js
|
|
20
|
+
let isShuttingDown = false;
|
|
21
|
+
|
|
22
|
+
const gracefulShutdown = () => {
|
|
23
|
+
if (isShuttingDown) return;
|
|
24
|
+
isShuttingDown = true;
|
|
25
|
+
|
|
26
|
+
console.log(`Worker ${process.pid} starting graceful shutdown...`);
|
|
27
|
+
|
|
28
|
+
server.close(() => {
|
|
29
|
+
console.log(`Worker ${process.pid} server closed`);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Set shutdown timeout
|
|
34
|
+
setTimeout(() => {
|
|
35
|
+
console.log(`Worker ${process.pid} shutdown timeout, forcing exit`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}, 5000);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Listen for shutdown messages from master process
|
|
41
|
+
process.on('message', (message) => {
|
|
42
|
+
if (message.type === 'SHUTDOWN') {
|
|
43
|
+
console.log(`Worker ${process.pid} received shutdown signal from master`);
|
|
44
|
+
gracefulShutdown();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Listen for system signals (as backup)
|
|
49
|
+
process.once('SIGTERM', () => {
|
|
50
|
+
console.log(`Worker ${process.pid} received SIGTERM`);
|
|
51
|
+
gracefulShutdown();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
process.once('SIGINT', () => {
|
|
55
|
+
console.log(`Worker ${process.pid} received SIGINT`);
|
|
56
|
+
gracefulShutdown();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
handleRequest: async (data, callback) => {
|
|
61
|
+
if (callback) {
|
|
62
|
+
callback({ pid: process.pid, data });
|
|
63
|
+
}
|
|
64
|
+
return { pid: process.pid, data };
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
close: (callback) => {
|
|
68
|
+
server.close(callback);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async function main() {
|
|
75
|
+
console.log('🚀 Starting Simple Cluster Example...');
|
|
76
|
+
|
|
77
|
+
const cluster = new ClusterManager({
|
|
78
|
+
workers: 2, // Fixed 2 worker processes
|
|
79
|
+
port: 3000,
|
|
80
|
+
gracefulShutdown: true,
|
|
81
|
+
shutdownTimeout: 10000
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Basic event listeners
|
|
85
|
+
cluster.on('worker:ready', (data) => {
|
|
86
|
+
console.log(`Worker ${data.pid} ready`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
cluster.on('cluster:ready', (data) => {
|
|
90
|
+
console.log(`Cluster ready on port ${data.port}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Listen for cluster shutdown events
|
|
94
|
+
cluster.on('cluster:shutdown', () => {
|
|
95
|
+
console.log('Cluster shutdown complete');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await cluster.start(createSimpleApp);
|
|
99
|
+
|
|
100
|
+
console.log('✅ Cluster running. Press Ctrl+C to stop.');
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (cluster.isPrimary) {
|
|
106
|
+
main().catch(console.error);
|
|
107
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// package/cluster/src/examples/benchmark-cluster.js
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import net from 'net';
|
|
4
|
+
import cluster from 'cluster';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { Worker } from 'worker_threads';
|
|
7
|
+
import ClusterManager from '../core/ClusterManager.js';
|
|
8
|
+
|
|
9
|
+
if (!cluster.isPrimary) {
|
|
10
|
+
const CORES = os.availableParallelism?.() || os.cpus().length;
|
|
11
|
+
const workerPool = [];
|
|
12
|
+
const callbacks = new Map();
|
|
13
|
+
let nextId = 0;
|
|
14
|
+
let workerIndex = 0;
|
|
15
|
+
|
|
16
|
+
console.log(`[Worker ${process.pid}] Starting ${CORES} persistent Worker Threads`);
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < CORES; i++) {
|
|
19
|
+
const worker = new Worker(`
|
|
20
|
+
const { parentPort } = require('worker_threads');
|
|
21
|
+
parentPort.on('message', ({ id }) => {
|
|
22
|
+
let sum = 0;
|
|
23
|
+
for (let i = 0; i < 60000; i++) { // Further reduce computation, target 4000+ req/s
|
|
24
|
+
sum += Math.random() * 0.1;
|
|
25
|
+
}
|
|
26
|
+
parentPort.postMessage({ id, result: sum.toFixed(2) });
|
|
27
|
+
});
|
|
28
|
+
`, { eval: true });
|
|
29
|
+
|
|
30
|
+
worker.on('message', ({ id, result }) => {
|
|
31
|
+
const cb = callbacks.get(id);
|
|
32
|
+
if (cb) {
|
|
33
|
+
cb(result);
|
|
34
|
+
callbacks.delete(id);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
workerPool.push(worker);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function heavyComputation() {
|
|
42
|
+
return new Promise(resolve => {
|
|
43
|
+
const id = nextId++;
|
|
44
|
+
callbacks.set(id, resolve);
|
|
45
|
+
const worker = workerPool[workerIndex];
|
|
46
|
+
workerIndex = (workerIndex + 1) % CORES;
|
|
47
|
+
worker.postMessage({ id });
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const server = http.createServer(async (req, res) => {
|
|
52
|
+
const start = process.hrtime.bigint();
|
|
53
|
+
|
|
54
|
+
if (req.url === '/heavy') {
|
|
55
|
+
await heavyComputation();
|
|
56
|
+
const latencyMs = Number(process.hrtime.bigint() - start) / 1_000_000;
|
|
57
|
+
|
|
58
|
+
res.writeHead(200, {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
'Server': 'HighPerf-WorkerPool'
|
|
61
|
+
});
|
|
62
|
+
res.end(`{"ok":true,"ms":${latencyMs.toFixed(2)}}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
67
|
+
res.end('{"status":"light"}');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
server.listen(3005, '127.0.0.1', () => {
|
|
71
|
+
console.log(`✅ Worker ${process.pid} listening on http://127.0.0.1:3005`);
|
|
72
|
+
if (process.send) process.send({ type: 'READY', pid: process.pid });
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ====================== Master Process ======================
|
|
77
|
+
async function isPortAvailable(port) {
|
|
78
|
+
return new Promise(r => {
|
|
79
|
+
const s = net.createServer()
|
|
80
|
+
.once('error', () => r(false))
|
|
81
|
+
.once('listening', () => { s.close(); r(true); })
|
|
82
|
+
.listen(port, '127.0.0.1');
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function main() {
|
|
87
|
+
console.log('🚀 Starting final high-performance version (Single Process + Optimized Thread Pool)...');
|
|
88
|
+
|
|
89
|
+
if (!(await isPortAvailable(3005))) {
|
|
90
|
+
console.error('❌ Port 3005 is already in use');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const manager = new ClusterManager({
|
|
95
|
+
workers: 1,
|
|
96
|
+
port: 3005
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
manager.on('worker:ready', () => {
|
|
100
|
+
console.log('\n🎉 Service is ready! Recommended benchmark:');
|
|
101
|
+
console.log('autocannon -c 1000 -d 30 -p 10 http://127.0.0.1:3005/heavy\n');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
manager.start();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (cluster.isPrimary) {
|
|
108
|
+
main().catch(err => {
|
|
109
|
+
console.error('Startup failed:', err);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// packages/cluster/src/examples/simple-app.js
|
|
2
|
+
import http from 'http';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Application factory function
|
|
6
|
+
* This function is called when each Worker process starts to create a service instance
|
|
7
|
+
*/
|
|
8
|
+
export default function createApp() {
|
|
9
|
+
// 1. Create HTTP server
|
|
10
|
+
const server = http.createServer((req, res) => {
|
|
11
|
+
// Log request
|
|
12
|
+
console.log(`[Worker ${process.pid}] Received request: ${req.method} ${req.url}`);
|
|
13
|
+
|
|
14
|
+
// Build response data
|
|
15
|
+
const responseData = {
|
|
16
|
+
status: 'ok',
|
|
17
|
+
pid: process.pid,
|
|
18
|
+
workerId: process.env.WORKER_ID || 'unknown',
|
|
19
|
+
message: 'Hello from Cluster Worker',
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
url: req.url
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Send response
|
|
25
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
26
|
+
res.end(JSON.stringify(responseData, null, 2));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// 2. Return object containing handleRequest method
|
|
30
|
+
// ClusterManager expects to interact with your application this way
|
|
31
|
+
return {
|
|
32
|
+
server, // Keep server reference for later closing
|
|
33
|
+
|
|
34
|
+
// ClusterManager will call this method to handle incoming requests or start listening
|
|
35
|
+
handleRequest: (req, res) => {
|
|
36
|
+
server.emit('request', req, res);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Optional: Method to start listening on a port
|
|
40
|
+
listen: (port, callback) => {
|
|
41
|
+
server.listen(port, () => {
|
|
42
|
+
console.log(`[Worker ${process.pid}] Server listening on port ${port}`);
|
|
43
|
+
if (callback) callback();
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Optional: Method to close the server
|
|
48
|
+
close: (callback) => {
|
|
49
|
+
server.close(callback);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|