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,78 @@
|
|
|
1
|
+
// packages/cluster/src/utils/cpu-detector.js
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CPUDetector - Utility class for detecting CPU capabilities and recommending worker counts
|
|
6
|
+
*/
|
|
7
|
+
class CPUDetector {
|
|
8
|
+
/**
|
|
9
|
+
* Get detailed CPU information
|
|
10
|
+
* @returns {Object} CPU information object
|
|
11
|
+
*/
|
|
12
|
+
static getInfo() {
|
|
13
|
+
const cpus = os.cpus();
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
model: cpus?.model || 'Unknown',
|
|
17
|
+
speed: cpus?.speed || 0, // in MHz
|
|
18
|
+
count: cpus.length,
|
|
19
|
+
architecture: os.arch(),
|
|
20
|
+
platform: os.platform(),
|
|
21
|
+
endianness: os.endianness(),
|
|
22
|
+
loadAvg: os.loadavg(),
|
|
23
|
+
uptime: os.uptime()
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculate the optimal number of worker processes
|
|
29
|
+
* Default strategy: Number of logical cores - 1 (to leave room for the master process)
|
|
30
|
+
* Minimum 1 worker.
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} options - Configuration options
|
|
33
|
+
* @param {number} options.reserve - Number of cores to reserve for system/master (default: 1)
|
|
34
|
+
* @param {number} options.max - Maximum number of workers allowed
|
|
35
|
+
* @returns {number} Recommended worker count
|
|
36
|
+
*/
|
|
37
|
+
static getOptimalWorkerCount(options = {}) {
|
|
38
|
+
const { reserve = 1, max = Infinity } = options;
|
|
39
|
+
const logicalCores = os.cpus().length;
|
|
40
|
+
|
|
41
|
+
// Calculate base count
|
|
42
|
+
let count = logicalCores - reserve;
|
|
43
|
+
|
|
44
|
+
// Ensure at least 1 worker
|
|
45
|
+
if (count < 1) {
|
|
46
|
+
count = 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Apply maximum limit
|
|
50
|
+
if (count > max) {
|
|
51
|
+
count = max;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return count;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if the current environment supports multi-core clustering
|
|
59
|
+
* @returns {boolean} True if multi-core is supported
|
|
60
|
+
*/
|
|
61
|
+
static isMultiCoreSupported() {
|
|
62
|
+
return os.cpus().length > 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get memory information relevant to clustering
|
|
67
|
+
* @returns {Object} Memory statistics in bytes
|
|
68
|
+
*/
|
|
69
|
+
static getMemoryInfo() {
|
|
70
|
+
return {
|
|
71
|
+
total: os.totalmem(),
|
|
72
|
+
free: os.freemem(),
|
|
73
|
+
used: os.totalmem() - os.freemem()
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default CPUDetector;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// packages/cluster/src/utils/env-loader.js
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 环境变量加载器
|
|
7
|
+
*/
|
|
8
|
+
export function loadEnv() {
|
|
9
|
+
const env = {};
|
|
10
|
+
|
|
11
|
+
// 默认值
|
|
12
|
+
const defaults = {
|
|
13
|
+
CLUSTER_ENABLED: 'true',
|
|
14
|
+
CLUSTER_WORKERS: 'auto',
|
|
15
|
+
CLUSTER_PORT: '3000',
|
|
16
|
+
CLUSTER_GRACEFUL_SHUTDOWN: 'true',
|
|
17
|
+
CLUSTER_RESTART_ON_EXIT: 'true',
|
|
18
|
+
CLUSTER_RESTART_DELAY: '1000',
|
|
19
|
+
CLUSTER_HEALTH_CHECK_ENABLED: 'true',
|
|
20
|
+
CLUSTER_HEALTH_CHECK_INTERVAL: '30000',
|
|
21
|
+
CLUSTER_HEALTH_CHECK_TIMEOUT: '5000',
|
|
22
|
+
CLUSTER_LOAD_BALANCING: 'true',
|
|
23
|
+
CLUSTER_LOAD_BALANCER_TYPE: 'round-robin',
|
|
24
|
+
CLUSTER_MAX_REQUESTS_PER_WORKER: '1000',
|
|
25
|
+
CLUSTER_MONITORING_ENABLED: 'true',
|
|
26
|
+
CLUSTER_MONITORING_INTERVAL: '10000',
|
|
27
|
+
CLUSTER_MEMORY_THRESHOLD: '0.8',
|
|
28
|
+
CLUSTER_CPU_THRESHOLD: '0.7',
|
|
29
|
+
CLUSTER_LOG_LEVEL: 'info',
|
|
30
|
+
CLUSTER_LOG_FORMAT: 'json',
|
|
31
|
+
CLUSTER_WORKER_REUSE_PORT: 'true',
|
|
32
|
+
CLUSTER_ENABLE_IPC: 'true',
|
|
33
|
+
CLUSTER_IPC_TIMEOUT: '5000',
|
|
34
|
+
CLUSTER_ENABLE_SECURITY: 'true',
|
|
35
|
+
CLUSTER_MAX_MEMORY: '512',
|
|
36
|
+
CLUSTER_MAX_OLD_SPACE_SIZE: '4096',
|
|
37
|
+
CLUSTER_DEBUG: 'false',
|
|
38
|
+
CLUSTER_DEBUG_PORT: '9229',
|
|
39
|
+
CLUSTER_INSPECT: 'false',
|
|
40
|
+
CLUSTER_ENABLE_HOT_RELOAD: 'false',
|
|
41
|
+
CLUSTER_ENABLE_PROFILING: 'false',
|
|
42
|
+
CLUSTER_PROFILING_INTERVAL: '60000'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// 从 process.env 加载
|
|
46
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
47
|
+
env[key] = process.env[key] || defaultValue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 尝试从 .env 文件加载
|
|
51
|
+
try {
|
|
52
|
+
const envPath = join(process.cwd(), '.env');
|
|
53
|
+
const envContent = readFileSync(envPath, 'utf8');
|
|
54
|
+
const envLines = envContent.split('\n');
|
|
55
|
+
|
|
56
|
+
for (const line of envLines) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
59
|
+
const [key, ...valueParts] = trimmed.split('=');
|
|
60
|
+
if (key && valueParts.length > 0) {
|
|
61
|
+
const value = valueParts.join('=').trim();
|
|
62
|
+
// 移除引号
|
|
63
|
+
const cleanValue = value.replace(/^['"]|['"]$/g, '');
|
|
64
|
+
env[key.trim()] = cleanValue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// .env 文件不存在,使用默认值
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 类型转换
|
|
73
|
+
const booleanKeys = [
|
|
74
|
+
'CLUSTER_ENABLED',
|
|
75
|
+
'CLUSTER_GRACEFUL_SHUTDOWN',
|
|
76
|
+
'CLUSTER_RESTART_ON_EXIT',
|
|
77
|
+
'CLUSTER_HEALTH_CHECK_ENABLED',
|
|
78
|
+
'CLUSTER_LOAD_BALANCING',
|
|
79
|
+
'CLUSTER_MONITORING_ENABLED',
|
|
80
|
+
'CLUSTER_WORKER_REUSE_PORT',
|
|
81
|
+
'CLUSTER_ENABLE_IPC',
|
|
82
|
+
'CLUSTER_ENABLE_SECURITY',
|
|
83
|
+
'CLUSTER_DEBUG',
|
|
84
|
+
'CLUSTER_INSPECT',
|
|
85
|
+
'CLUSTER_ENABLE_HOT_RELOAD',
|
|
86
|
+
'CLUSTER_ENABLE_PROFILING'
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const numberKeys = [
|
|
90
|
+
'CLUSTER_PORT',
|
|
91
|
+
'CLUSTER_RESTART_DELAY',
|
|
92
|
+
'CLUSTER_HEALTH_CHECK_INTERVAL',
|
|
93
|
+
'CLUSTER_HEALTH_CHECK_TIMEOUT',
|
|
94
|
+
'CLUSTER_MAX_REQUESTS_PER_WORKER',
|
|
95
|
+
'CLUSTER_MONITORING_INTERVAL',
|
|
96
|
+
'CLUSTER_IPC_TIMEOUT',
|
|
97
|
+
'CLUSTER_MAX_MEMORY',
|
|
98
|
+
'CLUSTER_MAX_OLD_SPACE_SIZE',
|
|
99
|
+
'CLUSTER_DEBUG_PORT',
|
|
100
|
+
'CLUSTER_PROFILING_INTERVAL'
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const floatKeys = [
|
|
104
|
+
'CLUSTER_MEMORY_THRESHOLD',
|
|
105
|
+
'CLUSTER_CPU_THRESHOLD'
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// 转换布尔值
|
|
109
|
+
for (const key of booleanKeys) {
|
|
110
|
+
if (env[key] !== undefined) {
|
|
111
|
+
env[key] = env[key] === 'true' || env[key] === '1';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 转换数字
|
|
116
|
+
for (const key of numberKeys) {
|
|
117
|
+
if (env[key] !== undefined) {
|
|
118
|
+
env[key] = parseInt(env[key], 10);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 转换浮点数
|
|
123
|
+
for (const key of floatKeys) {
|
|
124
|
+
if (env[key] !== undefined) {
|
|
125
|
+
env[key] = parseFloat(env[key]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 处理 workers 参数
|
|
130
|
+
if (env.CLUSTER_WORKERS === 'auto') {
|
|
131
|
+
env.CLUSTER_WORKERS = 'auto';
|
|
132
|
+
} else {
|
|
133
|
+
env.CLUSTER_WORKERS = parseInt(env.CLUSTER_WORKERS, 10);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return env;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 导出默认实例
|
|
140
|
+
export default { loadEnv };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// packages/cluster/src/utils/signal-handler.js
|
|
2
|
+
import process from 'process';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SignalHandler - Manages OS signals for graceful shutdown and restart
|
|
6
|
+
*/
|
|
7
|
+
class SignalHandler {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.handlers = new Map();
|
|
10
|
+
this.isShuttingDown = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register a handler for a specific signal
|
|
15
|
+
* @param {string} signal - Signal name (e.g., 'SIGINT', 'SIGTERM')
|
|
16
|
+
* @param {Function} callback - Async function to execute on signal
|
|
17
|
+
*/
|
|
18
|
+
on(signal, callback) {
|
|
19
|
+
if (!this.handlers.has(signal)) {
|
|
20
|
+
this.handlers.set(signal, []);
|
|
21
|
+
|
|
22
|
+
// Attach the listener to the process only once per signal type
|
|
23
|
+
process.on(signal, async () => {
|
|
24
|
+
if (this.isShuttingDown && signal !== 'SIGHUP') {
|
|
25
|
+
console.warn(`Received ${signal} while already shutting down. Forcing exit.`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (signal !== 'SIGHUP') {
|
|
31
|
+
this.isShuttingDown = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`\n📡 Received signal: ${signal}`);
|
|
35
|
+
|
|
36
|
+
const callbacks = this.handlers.get(signal);
|
|
37
|
+
try {
|
|
38
|
+
// Execute all registered handlers in parallel
|
|
39
|
+
await Promise.all(callbacks.map(cb => cb()));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`Error handling signal ${signal}:`, error);
|
|
42
|
+
} finally {
|
|
43
|
+
// Only exit on termination signals, not SIGHUP (reload)
|
|
44
|
+
if (signal !== 'SIGHUP') {
|
|
45
|
+
console.log('✅ Graceful shutdown complete.');
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.handlers.get(signal).push(callback);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Setup default handlers for common termination signals
|
|
57
|
+
* @param {Function} shutdownCallback - Optional global shutdown logic
|
|
58
|
+
*/
|
|
59
|
+
setupDefaults(shutdownCallback) {
|
|
60
|
+
// Handle Ctrl+C
|
|
61
|
+
this.on('SIGINT', async () => {
|
|
62
|
+
if (shutdownCallback) await shutdownCallback();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Handle kill command
|
|
66
|
+
this.on('SIGTERM', async () => {
|
|
67
|
+
if (shutdownCallback) await shutdownCallback();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Handle uncaught exceptions to prevent silent crashes
|
|
71
|
+
process.on('uncaughtException', (err) => {
|
|
72
|
+
console.error('💥 Uncaught Exception:', err.message);
|
|
73
|
+
console.error(err.stack);
|
|
74
|
+
// Attempt graceful shutdown on critical error
|
|
75
|
+
if (shutdownCallback) {
|
|
76
|
+
shutdownCallback().finally(() => process.exit(1));
|
|
77
|
+
} else {
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Handle unhandled promise rejections
|
|
83
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
84
|
+
console.error('⚠️ Unhandled Rejection at:', promise, 'reason:', reason);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Export singleton instance
|
|
90
|
+
export default new SignalHandler();
|