claw-dashboard 1.8.4 → 2.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/.c8rc.json +38 -0
- package/.dockerignore +68 -0
- package/.github/dependabot.yml +45 -0
- package/.github/pull_request_template.md +39 -0
- package/.github/workflows/ci.yml +147 -0
- package/.github/workflows/release.yml +40 -0
- package/.github/workflows/security.yml +84 -0
- package/.husky/pre-commit +1 -0
- package/.planning/codebase/ARCHITECTURE.md +206 -0
- package/.planning/codebase/INTEGRATIONS.md +150 -0
- package/.planning/codebase/STACK.md +122 -0
- package/.planning/codebase/STRUCTURE.md +201 -0
- package/CHANGELOG.md +293 -0
- package/CONTRIBUTING.md +378 -0
- package/Dockerfile +54 -0
- package/FEATURES.md +195 -21
- package/README.md +83 -6
- package/TODO.md +28 -0
- package/build-cjs.js +127 -0
- package/cjs-shim.js +13 -0
- package/dist/clawdash +1729 -0
- package/dist/clawdash.meta.json +2236 -0
- package/dist/widgets.cjs +6511 -0
- package/docker-compose.yml +67 -0
- package/docs/API.md +1050 -0
- package/docs/PLUGINS.md +1504 -0
- package/esbuild.config.js +158 -0
- package/eslint.config.js +56 -0
- package/examples/plugins/README.md +122 -0
- package/examples/plugins/api-status/index.js +294 -0
- package/examples/plugins/api-status/plugin.json +19 -0
- package/examples/plugins/hello-world/index.js +104 -0
- package/examples/plugins/hello-world/plugin.json +15 -0
- package/examples/plugins/system-metrics-chart/index.js +339 -0
- package/examples/plugins/system-metrics-chart/plugin.json +18 -0
- package/examples/plugins/weather-widget/index.js +136 -0
- package/examples/plugins/weather-widget/plugin.json +19 -0
- package/index.cjs +23005 -0
- package/index.js +5331 -512
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -5
- package/schemas/plugin-manifest.json +128 -0
- package/scripts/release.js +595 -0
- package/src/alerts.js +693 -0
- package/src/auto-save.js +584 -0
- package/src/cache.js +390 -0
- package/src/checksum.js +146 -0
- package/src/cli/args.js +110 -0
- package/src/cli/export-schedule.js +423 -0
- package/src/cli/export-snapshot.js +138 -0
- package/src/cli/help.js +69 -0
- package/src/cli/import-snapshot.js +230 -0
- package/src/cli/index.js +14 -0
- package/src/cli/list-templates.js +69 -0
- package/src/cli/validate-config.js +76 -0
- package/src/cli/validate-plugin.js +187 -0
- package/src/cli/version.js +15 -0
- package/src/config-validator.js +586 -0
- package/src/config-watcher.js +401 -0
- package/src/config.js +684 -0
- package/src/container-detector.js +499 -0
- package/src/database.js +734 -0
- package/src/differential-render.js +327 -0
- package/src/errors.js +169 -0
- package/src/export-scheduler.js +581 -0
- package/src/gateway-manager.js +685 -0
- package/src/hints.js +371 -0
- package/src/loading-states.js +509 -0
- package/src/logger.js +185 -0
- package/src/memory-pressure.js +467 -0
- package/src/performance-monitor.js +374 -0
- package/src/plugin-errors.js +586 -0
- package/src/plugin-manifest-validator.js +219 -0
- package/src/plugin-reload.js +481 -0
- package/src/plugin-scaffold.js +1941 -0
- package/src/plugin-validator.js +284 -0
- package/src/retry.js +218 -0
- package/src/security.js +860 -0
- package/src/snapshot.js +372 -0
- package/src/splash.js +115 -0
- package/src/theme-selector.js +411 -0
- package/src/themes.js +874 -0
- package/src/transitions.js +534 -0
- package/src/types.d.ts +174 -0
- package/src/validation.js +926 -0
- package/src/web-server.js +885 -0
- package/src/widgets/builtin-widgets.js +1057 -0
- package/src/widgets/config-processor.js +401 -0
- package/src/widgets/dependency-resolver.js +596 -0
- package/src/widgets/index.js +79 -0
- package/src/widgets/plugin-api.js +845 -0
- package/src/widgets/widget-error-boundary.js +773 -0
- package/src/widgets/widget-error-isolation.js +551 -0
- package/src/widgets/widget-loader.js +1437 -0
- package/src/workers/system-worker.js +130 -0
- package/src/workers/worker-pool.js +633 -0
- package/tests/alerts.test.js +437 -0
- package/tests/auto-save.test.js +529 -0
- package/tests/cache.test.js +317 -0
- package/tests/cli.test.js +351 -0
- package/tests/command-palette.test.js +320 -0
- package/tests/config-processor.test.js +452 -0
- package/tests/config-validator.test.js +710 -0
- package/tests/config-watcher.test.js +594 -0
- package/tests/config.test.js +755 -0
- package/tests/database.test.js +438 -0
- package/tests/errors.test.js +189 -0
- package/tests/example-plugins.test.js +624 -0
- package/tests/integration.test.js +1321 -0
- package/tests/loading-states.test.js +300 -0
- package/tests/manifest-validation-on-load.test.js +671 -0
- package/tests/performance-monitor.test.js +190 -0
- package/tests/plugin-api-rate-limit.test.js +302 -0
- package/tests/plugin-errors.test.js +311 -0
- package/tests/plugin-lifecycle-e2e.test.js +1036 -0
- package/tests/plugin-reload.test.js +764 -0
- package/tests/rate-limiter.test.js +539 -0
- package/tests/retry.test.js +308 -0
- package/tests/security.test.js +411 -0
- package/tests/settings-modal.test.js +226 -0
- package/tests/theme-selector.test.js +57 -0
- package/tests/utils.js +242 -0
- package/tests/utils.test.js +317 -0
- package/tests/validate-plugin-cli.test.js +359 -0
- package/tests/validation.test.js +837 -0
- package/tests/web-server.test.js +646 -0
- package/tests/widget-arrange-mode.test.js +183 -0
- package/tests/widget-config-hot-reload.test.js +465 -0
- package/tests/widget-dependency.test.js +591 -0
- package/tests/widget-error-boundary.test.js +749 -0
- package/tests/widget-error-isolation.test.js +469 -0
- package/tests/widget-integration.test.js +1147 -0
- package/tests/widget-refresh-intervals.test.js +284 -0
- package/tests/worker-pool.test.js +522 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Pool Manager for system information gathering
|
|
3
|
+
* Manages worker thread lifecycle and provides a simple interface
|
|
4
|
+
* for offloading heavy systeminformation calls
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Worker } from 'worker_threads';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
import logger from '../logger.js';
|
|
11
|
+
import config from '../config.js';
|
|
12
|
+
import { WorkerPoolOverloadError } from '../errors.js';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Degradation levels
|
|
18
|
+
const DEGRADATION_LEVELS = {
|
|
19
|
+
NONE: 'none',
|
|
20
|
+
WARNING: 'warning',
|
|
21
|
+
CRITICAL: 'critical',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Circuit breaker states
|
|
25
|
+
const CIRCUIT_STATES = {
|
|
26
|
+
CLOSED: 'closed',
|
|
27
|
+
OPEN: 'open',
|
|
28
|
+
HALF_OPEN: 'half_open',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Worker Pool Manager class
|
|
33
|
+
* Manages a pool of worker threads for parallel system information gathering
|
|
34
|
+
* with graceful degradation under overload conditions
|
|
35
|
+
*/
|
|
36
|
+
export const DegradationLevel = {
|
|
37
|
+
NONE: 'none',
|
|
38
|
+
WARNING: 'warning',
|
|
39
|
+
CRITICAL: 'critical',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
class WorkerPool {
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
this.workerPath = options.workerPath || join(__dirname, 'system-worker.js');
|
|
45
|
+
this.maxWorkers = options.maxWorkers || config.WORKERS?.MAX_WORKERS || 2;
|
|
46
|
+
this.taskTimeout = options.taskTimeout || config.WORKERS?.TASK_TIMEOUT || 10000;
|
|
47
|
+
this.enableWorkers = options.enableWorkers ?? config.WORKERS?.ENABLED ?? true;
|
|
48
|
+
|
|
49
|
+
this.workers = [];
|
|
50
|
+
this.taskQueue = [];
|
|
51
|
+
this.taskId = 0;
|
|
52
|
+
this.pendingTasks = new Map();
|
|
53
|
+
this.isShutdown = false;
|
|
54
|
+
|
|
55
|
+
// Graceful degradation settings
|
|
56
|
+
const degradationConfig = options.degradationConfig || config.WORKER_DEGRADATION || {};
|
|
57
|
+
this.degradationConfig = {
|
|
58
|
+
queue: degradationConfig.QUEUE || { WARNING_SIZE: 10, CRITICAL_SIZE: 25, MAX_SIZE: 50 },
|
|
59
|
+
utilization: degradationConfig.UTILIZATION || { WARNING_PCT: 75, CRITICAL_PCT: 90 },
|
|
60
|
+
strategies: degradationConfig.STRATEGIES || {
|
|
61
|
+
ADAPTIVE_TIMEOUT: { ENABLED: true, WARNING_MULTIPLIER: 1.5, CRITICAL_MULTIPLIER: 2.0 },
|
|
62
|
+
SHED_LOAD: { ENABLED: true, SHED_NON_CRITICAL: true },
|
|
63
|
+
CIRCUIT_BREAKER: { ENABLED: true, FAILURE_THRESHOLD: 5, RESET_TIMEOUT_MS: 30000 },
|
|
64
|
+
},
|
|
65
|
+
recovery: degradationConfig.RECOVERY || { COOLDOWN_MS: 5000, MIN_NORMAL_OPERATIONS: 5 },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Degradation state
|
|
69
|
+
this.degradationLevel = DEGRADATION_LEVELS.NONE;
|
|
70
|
+
this.degradationSince = null;
|
|
71
|
+
this.consecutiveFailures = 0;
|
|
72
|
+
this.circuitBreakerState = CIRCUIT_STATES.CLOSED;
|
|
73
|
+
this.circuitBreakerOpenedAt = null;
|
|
74
|
+
this.successfulOperations = 0;
|
|
75
|
+
this.overloadEvents = 0;
|
|
76
|
+
this.lastOverloadTime = null;
|
|
77
|
+
this.totalRejected = 0;
|
|
78
|
+
this.totalShed = 0;
|
|
79
|
+
|
|
80
|
+
// Check if worker threads are supported (Node.js 12+)
|
|
81
|
+
this.workersSupported = this.checkWorkerSupport();
|
|
82
|
+
|
|
83
|
+
if (this.enableWorkers && this.workersSupported) {
|
|
84
|
+
this.initializeWorkers();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if worker threads are supported
|
|
90
|
+
* @returns {boolean} True if worker threads are available
|
|
91
|
+
*/
|
|
92
|
+
checkWorkerSupport() {
|
|
93
|
+
try {
|
|
94
|
+
// Worker threads are available in Node.js 12+
|
|
95
|
+
const [major] = process.versions.node.split('.').map(Number);
|
|
96
|
+
if (major < 12) {
|
|
97
|
+
logger.info('Worker threads not available (Node.js < 12)');
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Worker threads are available - we'll verify when creating workers
|
|
102
|
+
// This is a preliminary check
|
|
103
|
+
return true;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.info('Worker threads not supported in this environment:', error.message);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Initialize worker threads
|
|
112
|
+
*/
|
|
113
|
+
initializeWorkers() {
|
|
114
|
+
try {
|
|
115
|
+
for (let i = 0; i < this.maxWorkers; i++) {
|
|
116
|
+
this.createWorker(i);
|
|
117
|
+
}
|
|
118
|
+
logger.info(`Initialized ${this.maxWorkers} system information worker threads`);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.error('Failed to initialize workers:', error.message);
|
|
121
|
+
this.workersSupported = false;
|
|
122
|
+
this.workers = [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a new worker thread
|
|
128
|
+
* @param {number} id - Worker ID
|
|
129
|
+
* @returns {Worker} Created worker
|
|
130
|
+
*/
|
|
131
|
+
createWorker(id) {
|
|
132
|
+
const worker = new Worker(this.workerPath, {
|
|
133
|
+
execArgv: ['--experimental-vm-modules'],
|
|
134
|
+
workerData: { __filename: this.workerPath }
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
worker.id = id;
|
|
138
|
+
worker.isReady = false;
|
|
139
|
+
worker.isBusy = false;
|
|
140
|
+
|
|
141
|
+
worker.on('message', (message) => {
|
|
142
|
+
this.handleWorkerMessage(worker, message);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
worker.on('error', (error) => {
|
|
146
|
+
logger.error(`Worker ${id} error:`, error.message);
|
|
147
|
+
this.restartWorker(id);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
worker.on('exit', (code) => {
|
|
151
|
+
if (code !== 0) {
|
|
152
|
+
logger.warn(`Worker ${id} exited with code ${code}`);
|
|
153
|
+
}
|
|
154
|
+
this.removeWorker(id);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.workers.push(worker);
|
|
158
|
+
return worker;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Handle messages from worker threads
|
|
163
|
+
* @param {Worker} worker - Worker that sent the message
|
|
164
|
+
* @param {Object} message - Message from worker
|
|
165
|
+
*/
|
|
166
|
+
handleWorkerMessage(worker, message) {
|
|
167
|
+
// Handle ready signal
|
|
168
|
+
if (message.type === 'ready') {
|
|
169
|
+
worker.isReady = true;
|
|
170
|
+
this.processQueue();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Handle task completion
|
|
175
|
+
if (message.id !== undefined) {
|
|
176
|
+
const task = this.pendingTasks.get(message.id);
|
|
177
|
+
if (task) {
|
|
178
|
+
this.pendingTasks.delete(message.id);
|
|
179
|
+
worker.isBusy = false;
|
|
180
|
+
|
|
181
|
+
if (task.timeout) {
|
|
182
|
+
clearTimeout(task.timeout);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (message.success) {
|
|
186
|
+
this.recordSuccess();
|
|
187
|
+
task.resolve(message.data);
|
|
188
|
+
} else {
|
|
189
|
+
this.recordFailure();
|
|
190
|
+
const error = new Error(message.error || 'Worker task failed');
|
|
191
|
+
if (message.stack) {
|
|
192
|
+
error.stack = message.stack;
|
|
193
|
+
}
|
|
194
|
+
task.reject(error);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.processQueue();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Restart a worker thread
|
|
204
|
+
* @param {number} id - Worker ID to restart
|
|
205
|
+
*/
|
|
206
|
+
restartWorker(id) {
|
|
207
|
+
const existingWorker = this.workers.find(w => w.id === id);
|
|
208
|
+
if (existingWorker) {
|
|
209
|
+
existingWorker.terminate().catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Create replacement worker (unref'd to prevent timer from blocking shutdown)
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
if (!this.isShutdown) {
|
|
215
|
+
this.createWorker(id);
|
|
216
|
+
}
|
|
217
|
+
}, 100).unref();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Remove a worker from the pool
|
|
222
|
+
* @param {number} id - Worker ID to remove
|
|
223
|
+
*/
|
|
224
|
+
removeWorker(id) {
|
|
225
|
+
const index = this.workers.findIndex(w => w.id === id);
|
|
226
|
+
if (index !== -1) {
|
|
227
|
+
this.workers.splice(index, 1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check current system load and detect overload conditions
|
|
233
|
+
* @returns {Object} Load status with degradation level
|
|
234
|
+
*/
|
|
235
|
+
checkOverload() {
|
|
236
|
+
const queueSize = this.taskQueue.length;
|
|
237
|
+
const busyWorkers = this.workers.filter(w => w.isBusy).length;
|
|
238
|
+
const utilizationPercent = this.workers.length > 0
|
|
239
|
+
? Math.round((busyWorkers / this.workers.length) * 100)
|
|
240
|
+
: 0;
|
|
241
|
+
|
|
242
|
+
// Check circuit breaker
|
|
243
|
+
if (this.circuitBreakerState === CIRCUIT_STATES.OPEN) {
|
|
244
|
+
const timeOpen = Date.now() - this.circuitBreakerOpenedAt;
|
|
245
|
+
if (timeOpen >= this.degradationConfig.strategies.CIRCUIT_BREAKER.RESET_TIMEOUT_MS) {
|
|
246
|
+
// Try half-open state
|
|
247
|
+
this.circuitBreakerState = CIRCUIT_STATES.HALF_OPEN;
|
|
248
|
+
logger.info('Circuit breaker entering half-open state');
|
|
249
|
+
} else {
|
|
250
|
+
return {
|
|
251
|
+
level: DEGRADATION_LEVELS.CRITICAL,
|
|
252
|
+
queueSize,
|
|
253
|
+
utilizationPercent,
|
|
254
|
+
circuitOpen: true,
|
|
255
|
+
reason: 'circuit_breaker',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check queue size thresholds
|
|
261
|
+
const { WARNING_SIZE, CRITICAL_SIZE, MAX_SIZE } = this.degradationConfig.queue;
|
|
262
|
+
|
|
263
|
+
if (queueSize >= MAX_SIZE) {
|
|
264
|
+
return {
|
|
265
|
+
level: DEGRADATION_LEVELS.CRITICAL,
|
|
266
|
+
queueSize,
|
|
267
|
+
utilizationPercent,
|
|
268
|
+
reason: 'max_queue_size',
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (queueSize >= CRITICAL_SIZE || utilizationPercent >= this.degradationConfig.utilization.CRITICAL_PCT) {
|
|
273
|
+
return {
|
|
274
|
+
level: DEGRADATION_LEVELS.CRITICAL,
|
|
275
|
+
queueSize,
|
|
276
|
+
utilizationPercent,
|
|
277
|
+
reason: queueSize >= CRITICAL_SIZE ? 'queue_size' : 'utilization',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (queueSize >= WARNING_SIZE || utilizationPercent >= this.degradationConfig.utilization.WARNING_PCT) {
|
|
282
|
+
return {
|
|
283
|
+
level: DEGRADATION_LEVELS.WARNING,
|
|
284
|
+
queueSize,
|
|
285
|
+
utilizationPercent,
|
|
286
|
+
reason: queueSize >= WARNING_SIZE ? 'queue_size' : 'utilization',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
level: DEGRADATION_LEVELS.NONE,
|
|
292
|
+
queueSize,
|
|
293
|
+
utilizationPercent,
|
|
294
|
+
reason: null,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get adaptive timeout based on current degradation level
|
|
300
|
+
* @returns {number} Adjusted timeout in milliseconds
|
|
301
|
+
*/
|
|
302
|
+
getAdaptiveTimeout() {
|
|
303
|
+
const baseTimeout = this.taskTimeout;
|
|
304
|
+
|
|
305
|
+
if (!this.degradationConfig.strategies.ADAPTIVE_TIMEOUT.ENABLED) {
|
|
306
|
+
return baseTimeout;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
switch (this.degradationLevel) {
|
|
310
|
+
case DEGRADATION_LEVELS.CRITICAL:
|
|
311
|
+
return baseTimeout * this.degradationConfig.strategies.ADAPTIVE_TIMEOUT.CRITICAL_MULTIPLIER;
|
|
312
|
+
case DEGRADATION_LEVELS.WARNING:
|
|
313
|
+
return baseTimeout * this.degradationConfig.strategies.ADAPTIVE_TIMEOUT.WARNING_MULTIPLIER;
|
|
314
|
+
default:
|
|
315
|
+
return baseTimeout;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Update degradation level based on current load
|
|
321
|
+
* @param {Object} loadStatus - Result from checkOverload()
|
|
322
|
+
*/
|
|
323
|
+
updateDegradationLevel(loadStatus) {
|
|
324
|
+
const previousLevel = this.degradationLevel;
|
|
325
|
+
const newLevel = loadStatus.level;
|
|
326
|
+
|
|
327
|
+
if (previousLevel !== newLevel) {
|
|
328
|
+
this.degradationLevel = newLevel;
|
|
329
|
+
this.degradationSince = Date.now();
|
|
330
|
+
|
|
331
|
+
if (newLevel === DEGRADATION_LEVELS.CRITICAL) {
|
|
332
|
+
this.overloadEvents++;
|
|
333
|
+
this.lastOverloadTime = Date.now();
|
|
334
|
+
logger.warn(`Worker pool entering critical degradation: ${loadStatus.reason} (queue: ${loadStatus.queueSize}, utilization: ${loadStatus.utilizationPercent}%)`);
|
|
335
|
+
} else if (newLevel === DEGRADATION_LEVELS.WARNING) {
|
|
336
|
+
logger.warn(`Worker pool entering warning state: ${loadStatus.reason} (queue: ${loadStatus.queueSize}, utilization: ${loadStatus.utilizationPercent}%)`);
|
|
337
|
+
} else if (previousLevel !== DEGRADATION_LEVELS.NONE) {
|
|
338
|
+
logger.info(`Worker pool returning to normal operation from ${previousLevel}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Reset consecutive successes when level changes
|
|
343
|
+
if (previousLevel !== newLevel) {
|
|
344
|
+
this.successfulOperations = 0;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Record a successful operation for recovery tracking
|
|
350
|
+
*/
|
|
351
|
+
recordSuccess() {
|
|
352
|
+
this.consecutiveFailures = 0;
|
|
353
|
+
this.successfulOperations++;
|
|
354
|
+
|
|
355
|
+
// Close circuit breaker if in half-open state
|
|
356
|
+
if (this.circuitBreakerState === CIRCUIT_STATES.HALF_OPEN) {
|
|
357
|
+
this.circuitBreakerState = CIRCUIT_STATES.CLOSED;
|
|
358
|
+
this.circuitBreakerOpenedAt = null;
|
|
359
|
+
logger.info('Circuit breaker closed - service recovered');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Auto-recover if we've had enough successful operations
|
|
363
|
+
if (this.degradationLevel !== DEGRADATION_LEVELS.NONE) {
|
|
364
|
+
const cooldownElapsed = Date.now() - this.degradationSince >= this.degradationConfig.recovery.COOLDOWN_MS;
|
|
365
|
+
const minOpsMet = this.successfulOperations >= this.degradationConfig.recovery.MIN_NORMAL_OPERATIONS;
|
|
366
|
+
|
|
367
|
+
if (cooldownElapsed && minOpsMet) {
|
|
368
|
+
const loadStatus = this.checkOverload();
|
|
369
|
+
if (loadStatus.level === DEGRADATION_LEVELS.NONE) {
|
|
370
|
+
this.updateDegradationLevel(loadStatus);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Record a failed operation and potentially open circuit breaker
|
|
378
|
+
*/
|
|
379
|
+
recordFailure() {
|
|
380
|
+
this.consecutiveFailures++;
|
|
381
|
+
this.successfulOperations = 0;
|
|
382
|
+
|
|
383
|
+
const threshold = this.degradationConfig.strategies.CIRCUIT_BREAKER.FAILURE_THRESHOLD;
|
|
384
|
+
|
|
385
|
+
if (this.degradationConfig.strategies.CIRCUIT_BREAKER.ENABLED &&
|
|
386
|
+
this.consecutiveFailures >= threshold &&
|
|
387
|
+
this.circuitBreakerState === CIRCUIT_STATES.CLOSED) {
|
|
388
|
+
this.circuitBreakerState = CIRCUIT_STATES.OPEN;
|
|
389
|
+
this.circuitBreakerOpenedAt = Date.now();
|
|
390
|
+
logger.error(`Circuit breaker opened after ${this.consecutiveFailures} consecutive failures`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Check if we should reject/shed a new task due to overload
|
|
396
|
+
* @param {Object} options - Task options
|
|
397
|
+
* @returns {boolean} True if task should be rejected
|
|
398
|
+
*/
|
|
399
|
+
shouldShedLoad(options = {}) {
|
|
400
|
+
// Never shed critical tasks
|
|
401
|
+
if (options.critical || options.priority === 'high') {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Check if load shedding is enabled
|
|
406
|
+
if (!this.degradationConfig.strategies.SHED_LOAD.ENABLED) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Shed non-critical tasks when in critical state
|
|
411
|
+
if (this.degradationLevel === DEGRADATION_LEVELS.CRITICAL &&
|
|
412
|
+
this.degradationConfig.strategies.SHED_LOAD.SHED_NON_CRITICAL) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Execute a systeminformation command via worker thread
|
|
421
|
+
* @param {string} command - Command to execute
|
|
422
|
+
* @param {Object} options - Command options
|
|
423
|
+
* @returns {Promise<any>} Command result
|
|
424
|
+
*/
|
|
425
|
+
async execute(command, options = {}) {
|
|
426
|
+
// Check for overload conditions
|
|
427
|
+
const loadStatus = this.checkOverload();
|
|
428
|
+
this.updateDegradationLevel(loadStatus);
|
|
429
|
+
|
|
430
|
+
// Check if queue is at max capacity (reject before load shedding)
|
|
431
|
+
if (loadStatus.level === DEGRADATION_LEVELS.CRITICAL && loadStatus.reason === 'max_queue_size') {
|
|
432
|
+
this.totalRejected++;
|
|
433
|
+
throw new WorkerPoolOverloadError('Worker pool queue at maximum capacity', {
|
|
434
|
+
degradationLevel: this.degradationLevel,
|
|
435
|
+
queueSize: loadStatus.queueSize,
|
|
436
|
+
utilizationPercent: loadStatus.utilizationPercent,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Check if circuit breaker is open
|
|
441
|
+
if (loadStatus.circuitOpen) {
|
|
442
|
+
this.totalRejected++;
|
|
443
|
+
throw new WorkerPoolOverloadError('Worker pool circuit breaker is open', {
|
|
444
|
+
degradationLevel: this.degradationLevel,
|
|
445
|
+
queueSize: loadStatus.queueSize,
|
|
446
|
+
utilizationPercent: loadStatus.utilizationPercent,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Check if we should shed load (after max queue check)
|
|
451
|
+
if (this.shouldShedLoad(options)) {
|
|
452
|
+
this.totalShed++;
|
|
453
|
+
logger.debug(`Shedding load for command: ${command}`);
|
|
454
|
+
// Fall back to direct execution for shed tasks
|
|
455
|
+
try {
|
|
456
|
+
const result = await this.fallbackExecute(command, options);
|
|
457
|
+
this.recordSuccess();
|
|
458
|
+
return result;
|
|
459
|
+
} catch (error) {
|
|
460
|
+
this.recordFailure();
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Fall back to direct execution if workers aren't available
|
|
466
|
+
if (!this.workersSupported || !this.enableWorkers || this.workers.length === 0) {
|
|
467
|
+
try {
|
|
468
|
+
const result = await this.fallbackExecute(command, options);
|
|
469
|
+
this.recordSuccess();
|
|
470
|
+
return result;
|
|
471
|
+
} catch (error) {
|
|
472
|
+
this.recordFailure();
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return new Promise((resolve, reject) => {
|
|
478
|
+
const id = ++this.taskId;
|
|
479
|
+
|
|
480
|
+
// Use adaptive timeout based on degradation level
|
|
481
|
+
const adaptiveTimeout = this.getAdaptiveTimeout();
|
|
482
|
+
|
|
483
|
+
// Set up timeout with unref to prevent timer from keeping process alive
|
|
484
|
+
const timeout = setTimeout(() => {
|
|
485
|
+
this.pendingTasks.delete(id);
|
|
486
|
+
this.recordFailure();
|
|
487
|
+
reject(new Error(`Worker task timeout: ${command}`));
|
|
488
|
+
}, adaptiveTimeout).unref();
|
|
489
|
+
|
|
490
|
+
// Store task
|
|
491
|
+
this.pendingTasks.set(id, {
|
|
492
|
+
id,
|
|
493
|
+
command,
|
|
494
|
+
options,
|
|
495
|
+
resolve,
|
|
496
|
+
reject,
|
|
497
|
+
timeout,
|
|
498
|
+
timestamp: Date.now(),
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Add to queue
|
|
502
|
+
this.taskQueue.push({ id, command, options });
|
|
503
|
+
this.processQueue();
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Process the task queue
|
|
509
|
+
*/
|
|
510
|
+
processQueue() {
|
|
511
|
+
if (this.taskQueue.length === 0) return;
|
|
512
|
+
|
|
513
|
+
// Find an available worker
|
|
514
|
+
const availableWorker = this.workers.find(w => w.isReady && !w.isBusy);
|
|
515
|
+
if (!availableWorker) return;
|
|
516
|
+
|
|
517
|
+
// Get next task
|
|
518
|
+
const task = this.taskQueue.shift();
|
|
519
|
+
if (!task) return;
|
|
520
|
+
|
|
521
|
+
// Mark worker as busy and send task
|
|
522
|
+
availableWorker.isBusy = true;
|
|
523
|
+
availableWorker.postMessage({
|
|
524
|
+
id: task.id,
|
|
525
|
+
command: task.command,
|
|
526
|
+
options: task.options,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Fallback execution when workers aren't available
|
|
532
|
+
* Executes directly in the main thread
|
|
533
|
+
* @param {string} command - Command to execute
|
|
534
|
+
* @param {Object} options - Command options
|
|
535
|
+
* @returns {Promise<any>} Command result
|
|
536
|
+
*/
|
|
537
|
+
async fallbackExecute(command, options = {}) {
|
|
538
|
+
try {
|
|
539
|
+
// Dynamically import systeminformation
|
|
540
|
+
const si = await import('systeminformation');
|
|
541
|
+
const systemInfo = si.default || si;
|
|
542
|
+
|
|
543
|
+
switch (command) {
|
|
544
|
+
case 'currentLoad':
|
|
545
|
+
return await systemInfo.currentLoad();
|
|
546
|
+
case 'mem':
|
|
547
|
+
return await systemInfo.mem();
|
|
548
|
+
case 'graphics':
|
|
549
|
+
return await systemInfo.graphics();
|
|
550
|
+
case 'networkStats':
|
|
551
|
+
return await systemInfo.networkStats();
|
|
552
|
+
case 'fsSize':
|
|
553
|
+
return await systemInfo.fsSize();
|
|
554
|
+
case 'systemData': {
|
|
555
|
+
const [os, ver, time] = await Promise.all([
|
|
556
|
+
systemInfo.osInfo(),
|
|
557
|
+
systemInfo.versions(),
|
|
558
|
+
systemInfo.time(),
|
|
559
|
+
]);
|
|
560
|
+
return { os, ver, time };
|
|
561
|
+
}
|
|
562
|
+
default:
|
|
563
|
+
throw new Error(`Unknown command: ${command}`);
|
|
564
|
+
}
|
|
565
|
+
} catch (error) {
|
|
566
|
+
logger.warn(`Fallback execution failed for ${command}:`, error.message);
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Get worker pool status
|
|
573
|
+
* @returns {Object} Pool status
|
|
574
|
+
*/
|
|
575
|
+
getStatus() {
|
|
576
|
+
const loadStatus = this.checkOverload();
|
|
577
|
+
return {
|
|
578
|
+
enabled: this.enableWorkers,
|
|
579
|
+
supported: this.workersSupported,
|
|
580
|
+
totalWorkers: this.workers.length,
|
|
581
|
+
busyWorkers: this.workers.filter(w => w.isBusy).length,
|
|
582
|
+
readyWorkers: this.workers.filter(w => w.isReady).length,
|
|
583
|
+
pendingTasks: this.pendingTasks.size,
|
|
584
|
+
queuedTasks: this.taskQueue.length,
|
|
585
|
+
degradation: {
|
|
586
|
+
level: this.degradationLevel,
|
|
587
|
+
since: this.degradationSince,
|
|
588
|
+
queueSize: loadStatus.queueSize,
|
|
589
|
+
utilizationPercent: loadStatus.utilizationPercent,
|
|
590
|
+
circuitBreakerState: this.circuitBreakerState,
|
|
591
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
592
|
+
successfulOperations: this.successfulOperations,
|
|
593
|
+
overloadEvents: this.overloadEvents,
|
|
594
|
+
lastOverloadTime: this.lastOverloadTime,
|
|
595
|
+
totalRejected: this.totalRejected,
|
|
596
|
+
totalShed: this.totalShed,
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Shut down all workers
|
|
603
|
+
*/
|
|
604
|
+
async shutdown() {
|
|
605
|
+
this.isShutdown = true;
|
|
606
|
+
|
|
607
|
+
// Reject pending tasks
|
|
608
|
+
for (const [id, task] of this.pendingTasks) {
|
|
609
|
+
if (task.timeout) {
|
|
610
|
+
clearTimeout(task.timeout);
|
|
611
|
+
}
|
|
612
|
+
task.reject(new Error('Worker pool shutting down'));
|
|
613
|
+
}
|
|
614
|
+
this.pendingTasks.clear();
|
|
615
|
+
this.taskQueue = [];
|
|
616
|
+
|
|
617
|
+
// Terminate all workers
|
|
618
|
+
const terminationPromises = this.workers.map(worker =>
|
|
619
|
+
worker.terminate().catch(() => {})
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
await Promise.all(terminationPromises);
|
|
623
|
+
this.workers = [];
|
|
624
|
+
|
|
625
|
+
logger.info('Worker pool shut down');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Create singleton instance
|
|
630
|
+
const workerPool = new WorkerPool();
|
|
631
|
+
|
|
632
|
+
export default workerPool;
|
|
633
|
+
export { WorkerPool, DEGRADATION_LEVELS, CIRCUIT_STATES };
|