@rosepetal/node-red-contrib-async-function 1.0.0 → 1.0.2
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/README.md +4 -3
- package/nodes/async-function.html +65 -12
- package/nodes/async-function.js +132 -48
- package/nodes/lib/child-process-pool.js +610 -0
- package/nodes/lib/child-process-script.js +264 -0
- package/nodes/lib/message-serializer.js +1 -1
- package/nodes/lib/worker-pool.js +12 -0
- package/nodes/lib/worker-script.js +132 -69
- package/package.json +1 -1
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child Process Pool Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages a pool of child processes for executing async function code.
|
|
5
|
+
* Handles task queuing, process lifecycle, timeouts, and error recovery.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { fork } = require('child_process');
|
|
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 process 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, 'child-process-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 ChildProcessPool {
|
|
34
|
+
/**
|
|
35
|
+
* Create a child process 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 process pool
|
|
55
|
+
* @returns {Promise<void>}
|
|
56
|
+
*/
|
|
57
|
+
async initialize() {
|
|
58
|
+
if (this.initialized) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const promises = [];
|
|
63
|
+
for (let i = 0; i < this.config.numWorkers; i++) {
|
|
64
|
+
promises.push(this.createWorker());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await Promise.all(promises);
|
|
68
|
+
this.initialized = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a new child process
|
|
73
|
+
* @returns {Promise<object>} Worker state object
|
|
74
|
+
*/
|
|
75
|
+
createWorker() {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
let settled = false;
|
|
78
|
+
|
|
79
|
+
const child = fork(this.config.workerScript, [], {
|
|
80
|
+
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
81
|
+
serialization: 'advanced'
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const workerState = {
|
|
85
|
+
worker: child,
|
|
86
|
+
state: WorkerState.STARTING,
|
|
87
|
+
taskId: null,
|
|
88
|
+
startTime: Date.now(),
|
|
89
|
+
markedForRemoval: false
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const readyTimeout = setTimeout(() => {
|
|
93
|
+
if (settled) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
settled = true;
|
|
97
|
+
try {
|
|
98
|
+
child.kill();
|
|
99
|
+
} catch (_err) {
|
|
100
|
+
// Ignore kill errors
|
|
101
|
+
}
|
|
102
|
+
reject(new Error('Worker failed to start within timeout'));
|
|
103
|
+
}, 5000);
|
|
104
|
+
|
|
105
|
+
const handleReady = (msg) => {
|
|
106
|
+
if (!msg || typeof msg !== 'object') {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (msg.type === 'ready') {
|
|
111
|
+
clearTimeout(readyTimeout);
|
|
112
|
+
if (settled) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
settled = true;
|
|
116
|
+
|
|
117
|
+
if (msg.failedModules && msg.failedModules.length > 0) {
|
|
118
|
+
const detail = msg.failedModules
|
|
119
|
+
.map((failed) => `${failed.module} (${failed.var}): ${failed.error}`)
|
|
120
|
+
.join('; ');
|
|
121
|
+
const err = new Error(`Worker failed to load module(s): ${detail}`);
|
|
122
|
+
err.failedModules = msg.failedModules;
|
|
123
|
+
try {
|
|
124
|
+
child.kill();
|
|
125
|
+
} catch (_err) {
|
|
126
|
+
// Ignore kill errors
|
|
127
|
+
}
|
|
128
|
+
reject(err);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
workerState.state = WorkerState.IDLE;
|
|
133
|
+
this.workers.push(workerState);
|
|
134
|
+
resolve(workerState);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.handleWorkerMessage(workerState, msg);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handleError = (err) => {
|
|
142
|
+
if (workerState.state === WorkerState.STARTING && !settled) {
|
|
143
|
+
settled = true;
|
|
144
|
+
clearTimeout(readyTimeout);
|
|
145
|
+
reject(err);
|
|
146
|
+
}
|
|
147
|
+
this.handleWorkerError(workerState, err);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const handleExit = (code, signal) => {
|
|
151
|
+
if (workerState.state === WorkerState.STARTING && !settled) {
|
|
152
|
+
settled = true;
|
|
153
|
+
clearTimeout(readyTimeout);
|
|
154
|
+
const reason = code !== null ? `code ${code}` : `signal ${signal}`;
|
|
155
|
+
reject(new Error(`Worker exited during startup (${reason})`));
|
|
156
|
+
}
|
|
157
|
+
this.handleWorkerExit(workerState, code, signal);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
child.on('message', handleReady);
|
|
161
|
+
child.on('error', handleError);
|
|
162
|
+
child.on('exit', handleExit);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
child.send({
|
|
166
|
+
type: 'init',
|
|
167
|
+
libs: this.config.libs || [],
|
|
168
|
+
nodeRedUserDir: this.config.nodeRedUserDir,
|
|
169
|
+
shmThreshold: this.config.shmThreshold
|
|
170
|
+
});
|
|
171
|
+
} catch (err) {
|
|
172
|
+
clearTimeout(readyTimeout);
|
|
173
|
+
try {
|
|
174
|
+
child.kill();
|
|
175
|
+
} catch (_killErr) {
|
|
176
|
+
// Ignore kill errors
|
|
177
|
+
}
|
|
178
|
+
reject(err);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resize the process pool gracefully
|
|
185
|
+
* @param {number} newNumWorkers - New target process count
|
|
186
|
+
* @returns {Promise<void>}
|
|
187
|
+
*/
|
|
188
|
+
async resizePool(newNumWorkers) {
|
|
189
|
+
if (this.shuttingDown) {
|
|
190
|
+
throw new Error('Cannot resize pool during shutdown');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (newNumWorkers < 1) {
|
|
194
|
+
throw new Error('numWorkers must be at least 1');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const currentCount = this.workers.length;
|
|
198
|
+
const delta = newNumWorkers - currentCount;
|
|
199
|
+
|
|
200
|
+
if (delta === 0) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (delta > 0) {
|
|
205
|
+
const promises = [];
|
|
206
|
+
for (let i = 0; i < delta; i++) {
|
|
207
|
+
promises.push(this.createWorker());
|
|
208
|
+
}
|
|
209
|
+
await Promise.all(promises);
|
|
210
|
+
} else {
|
|
211
|
+
const toRemoveCount = Math.abs(delta);
|
|
212
|
+
const idleWorkers = this.workers.filter(w => w.state === WorkerState.IDLE);
|
|
213
|
+
|
|
214
|
+
const toTerminate = idleWorkers.slice(0, toRemoveCount);
|
|
215
|
+
await Promise.all(toTerminate.map(w => this.terminateWorker(w)));
|
|
216
|
+
|
|
217
|
+
const remainingToRemove = toRemoveCount - toTerminate.length;
|
|
218
|
+
if (remainingToRemove > 0) {
|
|
219
|
+
const busyWorkers = this.workers.filter(w => w.state === WorkerState.BUSY);
|
|
220
|
+
for (let i = 0; i < remainingToRemove && i < busyWorkers.length; i++) {
|
|
221
|
+
busyWorkers[i].markedForRemoval = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.config.numWorkers = newNumWorkers;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get an idle worker
|
|
231
|
+
* @returns {object|null} Worker state object or null if none available
|
|
232
|
+
*/
|
|
233
|
+
async acquireWorker() {
|
|
234
|
+
return this.workers.find(w => w.state === WorkerState.IDLE) || null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Execute a task on a worker
|
|
239
|
+
* @param {string} code - User function code
|
|
240
|
+
* @param {object} msg - Message object
|
|
241
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
242
|
+
* @returns {Promise<object>} Result
|
|
243
|
+
*/
|
|
244
|
+
async executeTask(code, msg, timeout = this.config.taskTimeout) {
|
|
245
|
+
if (!this.initialized) {
|
|
246
|
+
await this.initialize();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (this.shuttingDown) {
|
|
250
|
+
throw new Error('Worker pool is shutting down');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const taskId = this.nextTaskId++;
|
|
254
|
+
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
const callback = (err, payload) => {
|
|
257
|
+
this.shmManager.cleanupTask(taskId).catch(_cleanupErr => {});
|
|
258
|
+
|
|
259
|
+
if (err) {
|
|
260
|
+
reject(err);
|
|
261
|
+
} else {
|
|
262
|
+
resolve(payload);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
this.callbacks.set(taskId, callback);
|
|
267
|
+
|
|
268
|
+
this.acquireWorker().then(workerState => {
|
|
269
|
+
if (workerState) {
|
|
270
|
+
this.runTask(workerState, taskId, code, msg, timeout);
|
|
271
|
+
} else {
|
|
272
|
+
if (this.taskQueue.length >= this.config.maxQueueSize) {
|
|
273
|
+
this.callbacks.delete(taskId);
|
|
274
|
+
this.shmManager.cleanupTask(taskId).catch(_cleanupErr => {});
|
|
275
|
+
reject(new Error('Task queue full'));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.taskQueue.push({ taskId, code, msg, timeout, callback });
|
|
280
|
+
}
|
|
281
|
+
}).catch(err => {
|
|
282
|
+
this.callbacks.delete(taskId);
|
|
283
|
+
this.shmManager.cleanupTask(taskId).catch(_cleanupErr => {});
|
|
284
|
+
reject(err);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Run a task on a specific worker
|
|
291
|
+
* @param {object} workerState - Worker state object
|
|
292
|
+
* @param {number} taskId - Task ID
|
|
293
|
+
* @param {string} code - User function code
|
|
294
|
+
* @param {object} msg - Message object
|
|
295
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
296
|
+
*/
|
|
297
|
+
runTask(workerState, taskId, code, msg, timeout) {
|
|
298
|
+
workerState.state = WorkerState.BUSY;
|
|
299
|
+
workerState.taskId = taskId;
|
|
300
|
+
|
|
301
|
+
this.serializer.sanitizeMessage(msg, null, taskId).then(sanitizedMsg => {
|
|
302
|
+
this.timeoutManager.startTimeout(taskId, timeout, () => {
|
|
303
|
+
this.handleTimeout(workerState, taskId);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
workerState.worker.send({
|
|
308
|
+
type: 'execute',
|
|
309
|
+
taskId,
|
|
310
|
+
code,
|
|
311
|
+
msg: sanitizedMsg
|
|
312
|
+
});
|
|
313
|
+
} catch (err) {
|
|
314
|
+
this.handleWorkerError(workerState, err);
|
|
315
|
+
}
|
|
316
|
+
}).catch(err => {
|
|
317
|
+
const callback = this.callbacks.get(taskId);
|
|
318
|
+
if (callback) {
|
|
319
|
+
this.callbacks.delete(taskId);
|
|
320
|
+
callback(new Error(`Message sanitization failed: ${err.message}`), null);
|
|
321
|
+
}
|
|
322
|
+
this.recycleWorker(workerState);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Handle message from worker
|
|
328
|
+
* @param {object} workerState - Worker state object
|
|
329
|
+
* @param {object} message - Message from worker
|
|
330
|
+
*/
|
|
331
|
+
handleWorkerMessage(workerState, message) {
|
|
332
|
+
const { type, taskId, result, error, performance } = message || {};
|
|
333
|
+
|
|
334
|
+
if (type === 'result') {
|
|
335
|
+
this.timeoutManager.cancelTimeout(taskId);
|
|
336
|
+
|
|
337
|
+
const callback = this.callbacks.get(taskId);
|
|
338
|
+
if (callback) {
|
|
339
|
+
this.callbacks.delete(taskId);
|
|
340
|
+
this.recycleWorker(workerState);
|
|
341
|
+
|
|
342
|
+
this.serializer.restoreBuffers(result).then(restoredResult => {
|
|
343
|
+
callback(null, { result: restoredResult, performance: performance || null });
|
|
344
|
+
}).catch(restoreErr => {
|
|
345
|
+
callback(restoreErr instanceof Error ? restoreErr : new Error(String(restoreErr)), null);
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.recycleWorker(workerState);
|
|
351
|
+
|
|
352
|
+
} else if (type === 'error') {
|
|
353
|
+
this.timeoutManager.cancelTimeout(taskId);
|
|
354
|
+
|
|
355
|
+
const callback = this.callbacks.get(taskId);
|
|
356
|
+
if (callback) {
|
|
357
|
+
this.callbacks.delete(taskId);
|
|
358
|
+
const err = new Error(error && error.message ? error.message : 'Worker error');
|
|
359
|
+
if (error && error.stack) {
|
|
360
|
+
err.stack = error.stack;
|
|
361
|
+
}
|
|
362
|
+
if (error && error.name) {
|
|
363
|
+
err.name = error.name;
|
|
364
|
+
}
|
|
365
|
+
callback(err, null);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.recycleWorker(workerState);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Handle worker error
|
|
374
|
+
* @param {object} workerState - Worker state object
|
|
375
|
+
* @param {Error} err - Error object
|
|
376
|
+
*/
|
|
377
|
+
async handleWorkerError(workerState, err) {
|
|
378
|
+
if (workerState.state === WorkerState.TERMINATING) {
|
|
379
|
+
this.removeWorker(workerState);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
workerState.state = WorkerState.TERMINATING;
|
|
384
|
+
|
|
385
|
+
if (workerState.taskId !== null) {
|
|
386
|
+
this.timeoutManager.cancelTimeout(workerState.taskId);
|
|
387
|
+
|
|
388
|
+
this.shmManager.cleanupTask(workerState.taskId).catch(_cleanupErr => {});
|
|
389
|
+
|
|
390
|
+
const callback = this.callbacks.get(workerState.taskId);
|
|
391
|
+
if (callback) {
|
|
392
|
+
this.callbacks.delete(workerState.taskId);
|
|
393
|
+
callback(new Error(`Worker error: ${err.message}`), null);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.removeWorker(workerState);
|
|
398
|
+
|
|
399
|
+
if (this.workers.length < this.config.numWorkers && !this.shuttingDown) {
|
|
400
|
+
try {
|
|
401
|
+
await this.createWorker();
|
|
402
|
+
} catch (_createErr) {
|
|
403
|
+
// Failed to create replacement
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Handle worker exit
|
|
410
|
+
* @param {object} workerState - Worker state object
|
|
411
|
+
* @param {number} code - Exit code
|
|
412
|
+
* @param {string} signal - Exit signal
|
|
413
|
+
*/
|
|
414
|
+
async handleWorkerExit(workerState, code, signal) {
|
|
415
|
+
if (workerState.state === WorkerState.TERMINATING) {
|
|
416
|
+
this.removeWorker(workerState);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const reason = signal ? `signal ${signal}` : `code ${code !== null ? code : 'unknown'}`;
|
|
421
|
+
await this.handleWorkerError(workerState, new Error(`Worker exited (${reason})`));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Handle task timeout
|
|
426
|
+
* @param {object} workerState - Worker state object
|
|
427
|
+
* @param {number} taskId - Task ID
|
|
428
|
+
*/
|
|
429
|
+
async handleTimeout(workerState, taskId) {
|
|
430
|
+
this.shmManager.cleanupTask(taskId).catch(_cleanupErr => {});
|
|
431
|
+
|
|
432
|
+
workerState.state = WorkerState.TERMINATING;
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
await this.terminateWorker(workerState);
|
|
436
|
+
} catch (_err) {
|
|
437
|
+
// Ignore termination errors
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
this.removeWorker(workerState);
|
|
441
|
+
|
|
442
|
+
const callback = this.callbacks.get(taskId);
|
|
443
|
+
if (callback) {
|
|
444
|
+
this.callbacks.delete(taskId);
|
|
445
|
+
callback(new Error('Execution timeout'), null);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (this.workers.length < this.config.numWorkers && !this.shuttingDown) {
|
|
449
|
+
try {
|
|
450
|
+
await this.createWorker();
|
|
451
|
+
} catch (_err) {
|
|
452
|
+
// Failed to create replacement
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
this.processQueue();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Recycle a worker after task completion
|
|
461
|
+
* @param {object} workerState - Worker state object
|
|
462
|
+
*/
|
|
463
|
+
recycleWorker(workerState) {
|
|
464
|
+
workerState.state = WorkerState.IDLE;
|
|
465
|
+
workerState.taskId = null;
|
|
466
|
+
|
|
467
|
+
if (workerState.markedForRemoval) {
|
|
468
|
+
this.terminateWorker(workerState).catch(_err => {});
|
|
469
|
+
this.processQueue();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (this.taskQueue.length > 0) {
|
|
474
|
+
const task = this.taskQueue.shift();
|
|
475
|
+
this.runTask(workerState, task.taskId, task.code, task.msg, task.timeout);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Process the task queue
|
|
481
|
+
*/
|
|
482
|
+
processQueue() {
|
|
483
|
+
if (this.taskQueue.length === 0) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const workerState = this.workers.find(w => w.state === WorkerState.IDLE);
|
|
488
|
+
if (workerState) {
|
|
489
|
+
const task = this.taskQueue.shift();
|
|
490
|
+
this.runTask(workerState, task.taskId, task.code, task.msg, task.timeout);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Terminate a worker
|
|
496
|
+
* @param {object} workerState - Worker state object
|
|
497
|
+
*/
|
|
498
|
+
async terminateWorker(workerState) {
|
|
499
|
+
workerState.state = WorkerState.TERMINATING;
|
|
500
|
+
const child = workerState.worker;
|
|
501
|
+
|
|
502
|
+
await new Promise((resolve) => {
|
|
503
|
+
let settled = false;
|
|
504
|
+
const finish = () => {
|
|
505
|
+
if (settled) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
settled = true;
|
|
509
|
+
resolve();
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const timeout = setTimeout(() => {
|
|
513
|
+
if (!child.killed) {
|
|
514
|
+
try {
|
|
515
|
+
child.kill();
|
|
516
|
+
} catch (_err) {
|
|
517
|
+
// Ignore kill errors
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
finish();
|
|
521
|
+
}, 250);
|
|
522
|
+
|
|
523
|
+
child.once('exit', () => {
|
|
524
|
+
clearTimeout(timeout);
|
|
525
|
+
finish();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
child.once('error', () => {
|
|
529
|
+
clearTimeout(timeout);
|
|
530
|
+
finish();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
if (child.connected) {
|
|
534
|
+
try {
|
|
535
|
+
child.send({ type: 'terminate' });
|
|
536
|
+
} catch (_err) {
|
|
537
|
+
// Ignore send errors
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
try {
|
|
541
|
+
child.kill();
|
|
542
|
+
} catch (_err) {
|
|
543
|
+
// Ignore kill errors
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
this.removeWorker(workerState);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Remove a worker from the pool
|
|
553
|
+
* @param {object} workerState - Worker state object
|
|
554
|
+
*/
|
|
555
|
+
removeWorker(workerState) {
|
|
556
|
+
this.workers = this.workers.filter(w => w !== workerState);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Shutdown the worker pool
|
|
561
|
+
* @returns {Promise<void>}
|
|
562
|
+
*/
|
|
563
|
+
async shutdown() {
|
|
564
|
+
this.shuttingDown = true;
|
|
565
|
+
|
|
566
|
+
this.timeoutManager.clear();
|
|
567
|
+
|
|
568
|
+
for (const task of this.taskQueue) {
|
|
569
|
+
const callback = this.callbacks.get(task.taskId);
|
|
570
|
+
if (callback) {
|
|
571
|
+
this.callbacks.delete(task.taskId);
|
|
572
|
+
callback(new Error('Worker pool shutdown'), null);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
this.taskQueue = [];
|
|
576
|
+
|
|
577
|
+
const promises = this.workers.map(ws => this.terminateWorker(ws));
|
|
578
|
+
await Promise.all(promises);
|
|
579
|
+
|
|
580
|
+
await this.shmManager.cleanupAll();
|
|
581
|
+
|
|
582
|
+
this.initialized = false;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Get pool statistics
|
|
587
|
+
* @returns {object} Statistics object
|
|
588
|
+
*/
|
|
589
|
+
getStats() {
|
|
590
|
+
const idleWorkers = this.workers.filter(w => w.state === WorkerState.IDLE).length;
|
|
591
|
+
const busyWorkers = this.workers.filter(w => w.state === WorkerState.BUSY).length;
|
|
592
|
+
const markedForRemoval = this.workers.filter(w => w.markedForRemoval).length;
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
totalWorkers: this.workers.length,
|
|
596
|
+
targetWorkers: this.config.numWorkers,
|
|
597
|
+
idleWorkers,
|
|
598
|
+
busyWorkers,
|
|
599
|
+
markedForRemoval,
|
|
600
|
+
queuedTasks: this.taskQueue.length,
|
|
601
|
+
activeTasks: this.timeoutManager.getActiveCount(),
|
|
602
|
+
sharedMemory: this.shmManager.getStats(),
|
|
603
|
+
config: this.config
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
module.exports = {
|
|
609
|
+
ChildProcessPool
|
|
610
|
+
};
|