@rosepetal/node-red-contrib-async-function 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/LICENSE +13 -0
- package/README.md +213 -0
- package/assets/example.png +0 -0
- package/nodes/async-function.html +600 -0
- package/nodes/async-function.js +351 -0
- package/nodes/lib/message-serializer.js +407 -0
- package/nodes/lib/module-installer.js +105 -0
- package/nodes/lib/shared-memory-manager.js +311 -0
- package/nodes/lib/timeout-manager.js +139 -0
- package/nodes/lib/worker-pool.js +533 -0
- package/nodes/lib/worker-script.js +192 -0
- package/package.json +41 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Pool Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages a pool of worker threads for executing async function code.
|
|
5
|
+
* Handles task queuing, worker lifecycle, timeouts, and error recovery.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { Worker } = require('worker_threads');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const TimeoutManager = require('./timeout-manager');
|
|
11
|
+
const { SharedMemoryManager } = require('./shared-memory-manager');
|
|
12
|
+
const { AsyncMessageSerializer } = require('./message-serializer');
|
|
13
|
+
|
|
14
|
+
// Default configuration
|
|
15
|
+
const DEFAULT_CONFIG = {
|
|
16
|
+
numWorkers: 3, // Fixed worker count
|
|
17
|
+
taskTimeout: 30000, // Default task timeout: 30s
|
|
18
|
+
maxQueueSize: 100, // Max queued messages
|
|
19
|
+
shmThreshold: 0, // Always use shared memory for Buffers
|
|
20
|
+
libs: [], // External modules to load in workers
|
|
21
|
+
nodeRedUserDir: null, // Node-RED user directory for module resolution
|
|
22
|
+
workerScript: path.join(__dirname, 'worker-script.js')
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Worker states
|
|
26
|
+
const WorkerState = {
|
|
27
|
+
IDLE: 'idle',
|
|
28
|
+
BUSY: 'busy',
|
|
29
|
+
STARTING: 'starting',
|
|
30
|
+
TERMINATING: 'terminating'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
class WorkerPool {
|
|
34
|
+
/**
|
|
35
|
+
* Create a worker pool
|
|
36
|
+
* @param {object} config - Configuration options
|
|
37
|
+
*/
|
|
38
|
+
constructor(config = {}) {
|
|
39
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
40
|
+
this.workers = []; // Array of { worker, state, taskId, idleTimer }
|
|
41
|
+
this.taskQueue = []; // Array of { taskId, code, msg, callback, timeout }
|
|
42
|
+
this.nextTaskId = 0;
|
|
43
|
+
this.callbacks = new Map(); // taskId → callback
|
|
44
|
+
this.timeoutManager = new TimeoutManager();
|
|
45
|
+
this.initialized = false;
|
|
46
|
+
this.shuttingDown = false;
|
|
47
|
+
|
|
48
|
+
// Shared memory management
|
|
49
|
+
this.shmManager = new SharedMemoryManager({ threshold: this.config.shmThreshold });
|
|
50
|
+
this.serializer = new AsyncMessageSerializer(this.shmManager);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Initialize the worker pool
|
|
55
|
+
* @returns {Promise<void>}
|
|
56
|
+
*/
|
|
57
|
+
async initialize() {
|
|
58
|
+
if (this.initialized) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Create exactly numWorkers workers
|
|
63
|
+
const promises = [];
|
|
64
|
+
for (let i = 0; i < this.config.numWorkers; i++) {
|
|
65
|
+
promises.push(this.createWorker());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await Promise.all(promises);
|
|
69
|
+
this.initialized = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a new worker
|
|
74
|
+
* @returns {Promise<object>} Worker state object
|
|
75
|
+
*/
|
|
76
|
+
createWorker() {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
try {
|
|
79
|
+
const worker = new Worker(this.config.workerScript, {
|
|
80
|
+
workerData: {
|
|
81
|
+
shmThreshold: this.config.shmThreshold,
|
|
82
|
+
libs: this.config.libs || [],
|
|
83
|
+
nodeRedUserDir: this.config.nodeRedUserDir
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const workerState = {
|
|
88
|
+
worker,
|
|
89
|
+
state: WorkerState.STARTING,
|
|
90
|
+
taskId: null,
|
|
91
|
+
startTime: Date.now(),
|
|
92
|
+
markedForRemoval: false
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Setup event handlers
|
|
96
|
+
worker.on('message', (msg) => this.handleWorkerMessage(workerState, msg));
|
|
97
|
+
worker.on('error', (err) => this.handleWorkerError(workerState, err));
|
|
98
|
+
worker.on('exit', (code) => this.handleWorkerExit(workerState, code));
|
|
99
|
+
|
|
100
|
+
// Wait for ready signal
|
|
101
|
+
const readyTimeout = setTimeout(() => {
|
|
102
|
+
reject(new Error('Worker failed to start within timeout'));
|
|
103
|
+
}, 5000);
|
|
104
|
+
|
|
105
|
+
worker.on('message', (msg) => {
|
|
106
|
+
if (msg.type === 'ready') {
|
|
107
|
+
clearTimeout(readyTimeout);
|
|
108
|
+
workerState.state = WorkerState.IDLE;
|
|
109
|
+
this.workers.push(workerState);
|
|
110
|
+
resolve(workerState);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
} catch (err) {
|
|
115
|
+
reject(err);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resize the worker pool gracefully
|
|
122
|
+
* @param {number} newNumWorkers - New target worker count
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
*/
|
|
125
|
+
async resizePool(newNumWorkers) {
|
|
126
|
+
if (this.shuttingDown) {
|
|
127
|
+
throw new Error('Cannot resize pool during shutdown');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (newNumWorkers < 1) {
|
|
131
|
+
throw new Error('numWorkers must be at least 1');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const currentCount = this.workers.length;
|
|
135
|
+
const delta = newNumWorkers - currentCount;
|
|
136
|
+
|
|
137
|
+
if (delta === 0) {
|
|
138
|
+
return; // No change needed
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (delta > 0) {
|
|
142
|
+
// Scale up: Add new workers
|
|
143
|
+
const promises = [];
|
|
144
|
+
for (let i = 0; i < delta; i++) {
|
|
145
|
+
promises.push(this.createWorker());
|
|
146
|
+
}
|
|
147
|
+
await Promise.all(promises);
|
|
148
|
+
} else {
|
|
149
|
+
// Scale down: Gracefully remove workers
|
|
150
|
+
const toRemoveCount = Math.abs(delta);
|
|
151
|
+
const idleWorkers = this.workers.filter(w => w.state === WorkerState.IDLE);
|
|
152
|
+
|
|
153
|
+
// Immediately terminate idle workers
|
|
154
|
+
const toTerminate = idleWorkers.slice(0, toRemoveCount);
|
|
155
|
+
await Promise.all(toTerminate.map(w => this.terminateWorker(w)));
|
|
156
|
+
|
|
157
|
+
// Mark remaining busy workers for removal after task completion
|
|
158
|
+
const remainingToRemove = toRemoveCount - toTerminate.length;
|
|
159
|
+
if (remainingToRemove > 0) {
|
|
160
|
+
const busyWorkers = this.workers.filter(w => w.state === WorkerState.BUSY);
|
|
161
|
+
for (let i = 0; i < remainingToRemove && i < busyWorkers.length; i++) {
|
|
162
|
+
busyWorkers[i].markedForRemoval = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Update config
|
|
168
|
+
this.config.numWorkers = newNumWorkers;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get an idle worker
|
|
173
|
+
* @returns {object|null} Worker state object or null if none available
|
|
174
|
+
*/
|
|
175
|
+
async acquireWorker() {
|
|
176
|
+
// Find and return idle worker (no dynamic creation)
|
|
177
|
+
return this.workers.find(w => w.state === WorkerState.IDLE) || null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Execute a task on a worker
|
|
182
|
+
* @param {string} code - User function code
|
|
183
|
+
* @param {object} msg - Message object
|
|
184
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
185
|
+
* @returns {Promise<object>} Result
|
|
186
|
+
*/
|
|
187
|
+
async executeTask(code, msg, timeout = this.config.taskTimeout) {
|
|
188
|
+
if (!this.initialized) {
|
|
189
|
+
await this.initialize();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.shuttingDown) {
|
|
193
|
+
throw new Error('Worker pool is shutting down');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const taskId = this.nextTaskId++;
|
|
197
|
+
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const callback = (err, payload) => {
|
|
200
|
+
// Cleanup shared memory on completion (success or error)
|
|
201
|
+
this.shmManager.cleanupTask(taskId).catch(_cleanupErr => {
|
|
202
|
+
// Log but don't fail - task already completed
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (err) {
|
|
206
|
+
reject(err);
|
|
207
|
+
} else {
|
|
208
|
+
resolve(payload);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
this.callbacks.set(taskId, callback);
|
|
213
|
+
|
|
214
|
+
// Try to acquire a worker
|
|
215
|
+
this.acquireWorker().then(workerState => {
|
|
216
|
+
if (workerState) {
|
|
217
|
+
// Worker available, run task immediately
|
|
218
|
+
this.runTask(workerState, taskId, code, msg, timeout);
|
|
219
|
+
} else {
|
|
220
|
+
// No worker available, queue the task
|
|
221
|
+
if (this.taskQueue.length >= this.config.maxQueueSize) {
|
|
222
|
+
this.callbacks.delete(taskId);
|
|
223
|
+
// Cleanup shared memory on queue rejection
|
|
224
|
+
this.shmManager.cleanupTask(taskId).catch(_cleanupErr => {});
|
|
225
|
+
reject(new Error('Task queue full'));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.taskQueue.push({ taskId, code, msg, timeout, callback });
|
|
230
|
+
}
|
|
231
|
+
}).catch(err => {
|
|
232
|
+
this.callbacks.delete(taskId);
|
|
233
|
+
// Cleanup shared memory on error
|
|
234
|
+
this.shmManager.cleanupTask(taskId).catch(_cleanupErr => {});
|
|
235
|
+
reject(err);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Run a task on a specific worker
|
|
242
|
+
* @param {object} workerState - Worker state object
|
|
243
|
+
* @param {number} taskId - Task ID
|
|
244
|
+
* @param {string} code - User function code
|
|
245
|
+
* @param {object} msg - Message object
|
|
246
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
247
|
+
*/
|
|
248
|
+
runTask(workerState, taskId, code, msg, timeout) {
|
|
249
|
+
// Update worker state
|
|
250
|
+
workerState.state = WorkerState.BUSY;
|
|
251
|
+
workerState.taskId = taskId;
|
|
252
|
+
|
|
253
|
+
this.serializer.sanitizeMessage(msg, null, taskId).then(sanitizedMsg => {
|
|
254
|
+
// Start timeout after message preparation (matches hot-mode behavior)
|
|
255
|
+
this.timeoutManager.startTimeout(taskId, timeout, () => {
|
|
256
|
+
this.handleTimeout(workerState, taskId);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Send task to worker
|
|
260
|
+
workerState.worker.postMessage({
|
|
261
|
+
type: 'execute',
|
|
262
|
+
taskId,
|
|
263
|
+
code,
|
|
264
|
+
msg: sanitizedMsg
|
|
265
|
+
});
|
|
266
|
+
}).catch(err => {
|
|
267
|
+
// Fail task if message prep fails
|
|
268
|
+
const callback = this.callbacks.get(taskId);
|
|
269
|
+
if (callback) {
|
|
270
|
+
this.callbacks.delete(taskId);
|
|
271
|
+
callback(new Error(`Message sanitization failed: ${err.message}`), null);
|
|
272
|
+
}
|
|
273
|
+
this.recycleWorker(workerState);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Handle message from worker
|
|
279
|
+
* @param {object} workerState - Worker state object
|
|
280
|
+
* @param {object} message - Message from worker
|
|
281
|
+
*/
|
|
282
|
+
handleWorkerMessage(workerState, message) {
|
|
283
|
+
const { type, taskId, result, error, performance } = message;
|
|
284
|
+
|
|
285
|
+
if (type === 'result') {
|
|
286
|
+
// Task completed successfully
|
|
287
|
+
this.timeoutManager.cancelTimeout(taskId);
|
|
288
|
+
|
|
289
|
+
const callback = this.callbacks.get(taskId);
|
|
290
|
+
if (callback) {
|
|
291
|
+
this.callbacks.delete(taskId);
|
|
292
|
+
// Recycle worker immediately; result restoration happens asynchronously
|
|
293
|
+
this.recycleWorker(workerState);
|
|
294
|
+
|
|
295
|
+
this.serializer.restoreBuffers(result).then(restoredResult => {
|
|
296
|
+
callback(null, { result: restoredResult, performance: performance || null });
|
|
297
|
+
}).catch(restoreErr => {
|
|
298
|
+
callback(restoreErr instanceof Error ? restoreErr : new Error(String(restoreErr)), null);
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Return worker to idle state
|
|
304
|
+
this.recycleWorker(workerState);
|
|
305
|
+
|
|
306
|
+
} else if (type === 'error') {
|
|
307
|
+
// Task failed with error
|
|
308
|
+
this.timeoutManager.cancelTimeout(taskId);
|
|
309
|
+
|
|
310
|
+
const callback = this.callbacks.get(taskId);
|
|
311
|
+
if (callback) {
|
|
312
|
+
this.callbacks.delete(taskId);
|
|
313
|
+
const err = new Error(error.message);
|
|
314
|
+
err.stack = error.stack;
|
|
315
|
+
err.name = error.name;
|
|
316
|
+
callback(err, null);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Return worker to idle state
|
|
320
|
+
this.recycleWorker(workerState);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Handle worker error
|
|
326
|
+
* @param {object} workerState - Worker state object
|
|
327
|
+
* @param {Error} err - Error object
|
|
328
|
+
*/
|
|
329
|
+
async handleWorkerError(workerState, err) {
|
|
330
|
+
// Cancel timeout if task was running
|
|
331
|
+
if (workerState.taskId !== null) {
|
|
332
|
+
this.timeoutManager.cancelTimeout(workerState.taskId);
|
|
333
|
+
|
|
334
|
+
// Cleanup shared memory for crashed task
|
|
335
|
+
this.shmManager.cleanupTask(workerState.taskId).catch(_cleanupErr => {
|
|
336
|
+
// Log but don't fail
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const callback = this.callbacks.get(workerState.taskId);
|
|
340
|
+
if (callback) {
|
|
341
|
+
this.callbacks.delete(workerState.taskId);
|
|
342
|
+
callback(new Error(`Worker error: ${err.message}`), null);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Remove worker from pool
|
|
347
|
+
this.removeWorker(workerState);
|
|
348
|
+
|
|
349
|
+
// Create replacement if below target
|
|
350
|
+
if (this.workers.length < this.config.numWorkers && !this.shuttingDown) {
|
|
351
|
+
try {
|
|
352
|
+
await this.createWorker();
|
|
353
|
+
} catch (createErr) {
|
|
354
|
+
// Failed to create replacement
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Handle worker exit
|
|
361
|
+
* @param {object} workerState - Worker state object
|
|
362
|
+
* @param {number} code - Exit code
|
|
363
|
+
*/
|
|
364
|
+
async handleWorkerExit(workerState, code) {
|
|
365
|
+
if (code !== 0 && workerState.state !== WorkerState.TERMINATING) {
|
|
366
|
+
// Worker crashed unexpectedly
|
|
367
|
+
await this.handleWorkerError(workerState, new Error(`Worker exited with code ${code}`));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Handle task timeout
|
|
373
|
+
* @param {object} workerState - Worker state object
|
|
374
|
+
* @param {number} taskId - Task ID
|
|
375
|
+
*/
|
|
376
|
+
async handleTimeout(workerState, taskId) {
|
|
377
|
+
// Cleanup shared memory for timed out task
|
|
378
|
+
this.shmManager.cleanupTask(taskId).catch(_cleanupErr => {
|
|
379
|
+
// Log but don't fail
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Terminate the worker
|
|
383
|
+
workerState.state = WorkerState.TERMINATING;
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await workerState.worker.terminate();
|
|
387
|
+
} catch (err) {
|
|
388
|
+
// Ignore termination errors
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Remove from pool
|
|
392
|
+
this.removeWorker(workerState);
|
|
393
|
+
|
|
394
|
+
// Fail the task
|
|
395
|
+
const callback = this.callbacks.get(taskId);
|
|
396
|
+
if (callback) {
|
|
397
|
+
this.callbacks.delete(taskId);
|
|
398
|
+
callback(new Error('Execution timeout'), null);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Create replacement worker
|
|
402
|
+
if (this.workers.length < this.config.numWorkers && !this.shuttingDown) {
|
|
403
|
+
try {
|
|
404
|
+
await this.createWorker();
|
|
405
|
+
} catch (err) {
|
|
406
|
+
// Failed to create replacement
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Process next queued task
|
|
411
|
+
this.processQueue();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Recycle a worker after task completion
|
|
416
|
+
* @param {object} workerState - Worker state object
|
|
417
|
+
*/
|
|
418
|
+
recycleWorker(workerState) {
|
|
419
|
+
workerState.state = WorkerState.IDLE;
|
|
420
|
+
workerState.taskId = null;
|
|
421
|
+
|
|
422
|
+
// Check if marked for removal (for resize support)
|
|
423
|
+
if (workerState.markedForRemoval) {
|
|
424
|
+
this.terminateWorker(workerState).catch(_err => {
|
|
425
|
+
// Log but don't fail
|
|
426
|
+
});
|
|
427
|
+
this.processQueue();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Process next queued task
|
|
432
|
+
if (this.taskQueue.length > 0) {
|
|
433
|
+
const task = this.taskQueue.shift();
|
|
434
|
+
this.runTask(workerState, task.taskId, task.code, task.msg, task.timeout);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Process the task queue
|
|
440
|
+
*/
|
|
441
|
+
processQueue() {
|
|
442
|
+
if (this.taskQueue.length === 0) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Find idle worker
|
|
447
|
+
const workerState = this.workers.find(w => w.state === WorkerState.IDLE);
|
|
448
|
+
if (workerState) {
|
|
449
|
+
const task = this.taskQueue.shift();
|
|
450
|
+
this.runTask(workerState, task.taskId, task.code, task.msg, task.timeout);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Terminate a worker
|
|
456
|
+
* @param {object} workerState - Worker state object
|
|
457
|
+
*/
|
|
458
|
+
async terminateWorker(workerState) {
|
|
459
|
+
workerState.state = WorkerState.TERMINATING;
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
await workerState.worker.terminate();
|
|
463
|
+
} catch (err) {
|
|
464
|
+
// Ignore termination errors
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
this.removeWorker(workerState);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Remove a worker from the pool
|
|
472
|
+
* @param {object} workerState - Worker state object
|
|
473
|
+
*/
|
|
474
|
+
removeWorker(workerState) {
|
|
475
|
+
this.workers = this.workers.filter(w => w !== workerState);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Shutdown the worker pool
|
|
480
|
+
* @returns {Promise<void>}
|
|
481
|
+
*/
|
|
482
|
+
async shutdown() {
|
|
483
|
+
this.shuttingDown = true;
|
|
484
|
+
|
|
485
|
+
// Cancel all timeouts
|
|
486
|
+
this.timeoutManager.clear();
|
|
487
|
+
|
|
488
|
+
// Fail all queued tasks
|
|
489
|
+
for (const task of this.taskQueue) {
|
|
490
|
+
const callback = this.callbacks.get(task.taskId);
|
|
491
|
+
if (callback) {
|
|
492
|
+
this.callbacks.delete(task.taskId);
|
|
493
|
+
callback(new Error('Worker pool shutdown'), null);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
this.taskQueue = [];
|
|
497
|
+
|
|
498
|
+
// Terminate all workers
|
|
499
|
+
const promises = this.workers.map(ws => this.terminateWorker(ws));
|
|
500
|
+
await Promise.all(promises);
|
|
501
|
+
|
|
502
|
+
// Cleanup all shared memory attachments
|
|
503
|
+
await this.shmManager.cleanupAll();
|
|
504
|
+
|
|
505
|
+
this.initialized = false;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Get pool statistics
|
|
510
|
+
* @returns {object} Statistics object
|
|
511
|
+
*/
|
|
512
|
+
getStats() {
|
|
513
|
+
const idleWorkers = this.workers.filter(w => w.state === WorkerState.IDLE).length;
|
|
514
|
+
const busyWorkers = this.workers.filter(w => w.state === WorkerState.BUSY).length;
|
|
515
|
+
const markedForRemoval = this.workers.filter(w => w.markedForRemoval).length;
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
totalWorkers: this.workers.length,
|
|
519
|
+
targetWorkers: this.config.numWorkers,
|
|
520
|
+
idleWorkers,
|
|
521
|
+
busyWorkers,
|
|
522
|
+
markedForRemoval,
|
|
523
|
+
queuedTasks: this.taskQueue.length,
|
|
524
|
+
activeTasks: this.timeoutManager.getActiveCount(),
|
|
525
|
+
sharedMemory: this.shmManager.getStats(),
|
|
526
|
+
config: this.config
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
module.exports = {
|
|
532
|
+
WorkerPool
|
|
533
|
+
};
|