@rosepetal/node-red-contrib-async-function 1.0.0 → 1.0.1
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/nodes/lib/worker-pool.js +8 -0
- package/nodes/lib/worker-script.js +67 -52
- package/package.json +1 -1
package/nodes/lib/worker-pool.js
CHANGED
|
@@ -107,6 +107,14 @@ class WorkerPool {
|
|
|
107
107
|
clearTimeout(readyTimeout);
|
|
108
108
|
workerState.state = WorkerState.IDLE;
|
|
109
109
|
this.workers.push(workerState);
|
|
110
|
+
|
|
111
|
+
// Log module loading failures so users can diagnose issues
|
|
112
|
+
if (msg.failedModules && msg.failedModules.length > 0) {
|
|
113
|
+
for (const failed of msg.failedModules) {
|
|
114
|
+
console.warn(`[async-function] Worker failed to load module "${failed.module}" (${failed.var}): ${failed.error}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
110
118
|
resolve(workerState);
|
|
111
119
|
}
|
|
112
120
|
});
|
|
@@ -7,12 +7,17 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const { parentPort, workerData } = require('worker_threads');
|
|
10
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
10
11
|
const { SharedMemoryManager } = require('./shared-memory-manager');
|
|
11
12
|
const { AsyncMessageSerializer } = require('./message-serializer');
|
|
12
13
|
|
|
13
14
|
// Track worker state
|
|
14
15
|
let isTerminating = false;
|
|
15
16
|
|
|
17
|
+
// AsyncLocalStorage for tracking task context across async boundaries
|
|
18
|
+
// This ensures unhandled rejections can be attributed to the correct task
|
|
19
|
+
const taskContext = new AsyncLocalStorage();
|
|
20
|
+
|
|
16
21
|
function hrtimeDiffToMs(start) {
|
|
17
22
|
if (typeof start !== 'bigint') {
|
|
18
23
|
return 0;
|
|
@@ -58,6 +63,7 @@ function setCachedFunction(cacheKey, fn) {
|
|
|
58
63
|
const loadedModules = {};
|
|
59
64
|
const moduleVars = [];
|
|
60
65
|
const moduleValues = [];
|
|
66
|
+
const failedModules = []; // Track modules that failed to load
|
|
61
67
|
|
|
62
68
|
// Add Node-RED user directory to module search path for external modules
|
|
63
69
|
if (workerData && workerData.nodeRedUserDir) {
|
|
@@ -77,6 +83,7 @@ if (workerData && workerData.libs && Array.isArray(workerData.libs)) {
|
|
|
77
83
|
moduleValues.push(loadedModules[lib.var]);
|
|
78
84
|
} catch (err) {
|
|
79
85
|
console.error(`[async-function] Failed to load module ${lib.module}: ${err.message}`);
|
|
86
|
+
failedModules.push({ module: lib.module, var: lib.var, error: err.message });
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
}
|
|
@@ -96,55 +103,60 @@ if (parentPort) {
|
|
|
96
103
|
|
|
97
104
|
// Handle different message types
|
|
98
105
|
if (type === 'execute') {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const rawResult = await userFunction(restoredMsg, ...moduleValues);
|
|
117
|
-
const executionMs = hrtimeDiffToMs(execStart);
|
|
118
|
-
|
|
119
|
-
// Offload buffers in the result (large Buffers -> shared memory descriptors)
|
|
120
|
-
const encodeStart = process.hrtime.bigint();
|
|
121
|
-
const encodedResult = await serializer.sanitizeMessage(rawResult, null, taskId);
|
|
122
|
-
const transferToJsMs = hrtimeDiffToMs(encodeStart);
|
|
123
|
-
|
|
124
|
-
// Send result back to main thread
|
|
125
|
-
parentPort.postMessage({
|
|
126
|
-
type: 'result',
|
|
127
|
-
taskId,
|
|
128
|
-
result: encodedResult,
|
|
129
|
-
performance: {
|
|
130
|
-
transfer_to_python_ms: transferToPythonMs,
|
|
131
|
-
execution_ms: executionMs,
|
|
132
|
-
transfer_to_js_ms: transferToJsMs
|
|
106
|
+
// Run task within AsyncLocalStorage context for proper error attribution
|
|
107
|
+
// This ensures unhandled rejections from fire-and-forget promises
|
|
108
|
+
// can be traced back to the correct task
|
|
109
|
+
taskContext.run({ taskId }, async () => {
|
|
110
|
+
try {
|
|
111
|
+
// Restore + execute user code
|
|
112
|
+
const restoreStart = process.hrtime.bigint();
|
|
113
|
+
const restoredMsg = await serializer.restoreBuffers(msg);
|
|
114
|
+
const transferToPythonMs = hrtimeDiffToMs(restoreStart);
|
|
115
|
+
|
|
116
|
+
// Cache key includes code + module vars to handle different module configs
|
|
117
|
+
const cacheKey = code + '|' + moduleVars.join(',');
|
|
118
|
+
let userFunction = getCachedFunction(cacheKey);
|
|
119
|
+
if (!userFunction) {
|
|
120
|
+
// Create function with msg + all module variables as parameters
|
|
121
|
+
userFunction = new AsyncFunction('msg', ...moduleVars, code);
|
|
122
|
+
setCachedFunction(cacheKey, userFunction);
|
|
133
123
|
}
|
|
134
|
-
});
|
|
135
124
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
125
|
+
const execStart = process.hrtime.bigint();
|
|
126
|
+
// Execute with msg and all loaded module values
|
|
127
|
+
const rawResult = await userFunction(restoredMsg, ...moduleValues);
|
|
128
|
+
const executionMs = hrtimeDiffToMs(execStart);
|
|
129
|
+
|
|
130
|
+
// Offload buffers in the result (large Buffers -> shared memory descriptors)
|
|
131
|
+
const encodeStart = process.hrtime.bigint();
|
|
132
|
+
const encodedResult = await serializer.sanitizeMessage(rawResult, null, taskId);
|
|
133
|
+
const transferToJsMs = hrtimeDiffToMs(encodeStart);
|
|
134
|
+
|
|
135
|
+
// Send result back to main thread
|
|
136
|
+
parentPort.postMessage({
|
|
137
|
+
type: 'result',
|
|
138
|
+
taskId,
|
|
139
|
+
result: encodedResult,
|
|
140
|
+
performance: {
|
|
141
|
+
transfer_to_python_ms: transferToPythonMs,
|
|
142
|
+
execution_ms: executionMs,
|
|
143
|
+
transfer_to_js_ms: transferToJsMs
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
} catch (err) {
|
|
148
|
+
// Send error back to main thread
|
|
149
|
+
parentPort.postMessage({
|
|
150
|
+
type: 'error',
|
|
151
|
+
taskId,
|
|
152
|
+
error: {
|
|
153
|
+
message: err.message,
|
|
154
|
+
stack: err.stack,
|
|
155
|
+
name: err.name
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
148
160
|
} else if (type === 'terminate') {
|
|
149
161
|
// Graceful termination requested
|
|
150
162
|
isTerminating = true;
|
|
@@ -156,12 +168,13 @@ if (parentPort) {
|
|
|
156
168
|
}
|
|
157
169
|
});
|
|
158
170
|
|
|
159
|
-
// Handle errors
|
|
171
|
+
// Handle errors - use AsyncLocalStorage to get taskId for proper error attribution
|
|
160
172
|
process.on('uncaughtException', (err) => {
|
|
161
173
|
if (!isTerminating) {
|
|
174
|
+
const store = taskContext.getStore();
|
|
162
175
|
parentPort.postMessage({
|
|
163
176
|
type: 'error',
|
|
164
|
-
taskId: null,
|
|
177
|
+
taskId: store?.taskId ?? null,
|
|
165
178
|
error: {
|
|
166
179
|
message: `Uncaught exception: ${err.message}`,
|
|
167
180
|
stack: err.stack,
|
|
@@ -173,9 +186,10 @@ if (parentPort) {
|
|
|
173
186
|
|
|
174
187
|
process.on('unhandledRejection', (reason, _promise) => {
|
|
175
188
|
if (!isTerminating) {
|
|
189
|
+
const store = taskContext.getStore();
|
|
176
190
|
parentPort.postMessage({
|
|
177
191
|
type: 'error',
|
|
178
|
-
taskId: null,
|
|
192
|
+
taskId: store?.taskId ?? null,
|
|
179
193
|
error: {
|
|
180
194
|
message: `Unhandled rejection: ${reason}`,
|
|
181
195
|
stack: reason?.stack || '',
|
|
@@ -185,8 +199,9 @@ if (parentPort) {
|
|
|
185
199
|
}
|
|
186
200
|
});
|
|
187
201
|
|
|
188
|
-
// Signal ready
|
|
202
|
+
// Signal ready (include any module loading failures)
|
|
189
203
|
parentPort.postMessage({
|
|
190
|
-
type: 'ready'
|
|
204
|
+
type: 'ready',
|
|
205
|
+
failedModules: failedModules.length > 0 ? failedModules : undefined
|
|
191
206
|
});
|
|
192
207
|
}
|
package/package.json
CHANGED