@ondc/automation-mock-runner 1.3.16 → 1.3.18
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/dist/lib/MockRunner.js
CHANGED
|
@@ -456,7 +456,7 @@ class MockRunner {
|
|
|
456
456
|
const version = this.config.meta?.version || "2.0.0";
|
|
457
457
|
const majorVersion = parseInt(version.split(".")[0], 10);
|
|
458
458
|
// set city code
|
|
459
|
-
const cityCode = MockRunner.getIdFromSession(sessionData, "city_code")
|
|
459
|
+
const cityCode = MockRunner.getIdFromSession(sessionData, "city_code") ?? "*";
|
|
460
460
|
if (majorVersion === 1) {
|
|
461
461
|
return {
|
|
462
462
|
...baseContext,
|
|
@@ -2,15 +2,40 @@ import { FunctionSchema } from "../constants/function-registry";
|
|
|
2
2
|
import { ExecutionResult } from "../types/execution-results";
|
|
3
3
|
import { BaseCodeRunner } from "./base-runner";
|
|
4
4
|
export declare class NodeRunner implements BaseCodeRunner {
|
|
5
|
-
private
|
|
5
|
+
private pool;
|
|
6
|
+
private waitQueue;
|
|
6
7
|
private executionId;
|
|
7
|
-
private
|
|
8
|
+
private isTerminating;
|
|
8
9
|
private readonly workerPath;
|
|
9
10
|
private readonly maxMemoryMB;
|
|
11
|
+
private readonly poolSize;
|
|
12
|
+
private readonly maxExecutionsPerWorker;
|
|
13
|
+
private readonly maxWorkerAgeMs;
|
|
10
14
|
constructor(options?: {
|
|
11
15
|
maxMemoryMB?: number;
|
|
16
|
+
poolSize?: number;
|
|
17
|
+
maxExecutionsPerWorker?: number;
|
|
18
|
+
maxWorkerAgeMs?: number;
|
|
12
19
|
});
|
|
13
|
-
private
|
|
20
|
+
private createPooledWorker;
|
|
21
|
+
/**
|
|
22
|
+
* Terminate old worker (frees its V8 isolate) and replace with a fresh one.
|
|
23
|
+
*/
|
|
24
|
+
private replaceWorker;
|
|
25
|
+
private shouldRecycle;
|
|
26
|
+
private acquire;
|
|
27
|
+
private release;
|
|
14
28
|
execute(functionBody: string, schema: FunctionSchema, args: any[]): Promise<ExecutionResult>;
|
|
15
|
-
terminate(): void
|
|
29
|
+
terminate(): Promise<void>;
|
|
30
|
+
getStats(): {
|
|
31
|
+
poolSize: number;
|
|
32
|
+
busy: number;
|
|
33
|
+
idle: number;
|
|
34
|
+
waitingInQueue: number;
|
|
35
|
+
workers: {
|
|
36
|
+
busy: boolean;
|
|
37
|
+
executionCount: number;
|
|
38
|
+
ageSeconds: string;
|
|
39
|
+
}[];
|
|
40
|
+
};
|
|
16
41
|
}
|
|
@@ -37,86 +37,108 @@ exports.NodeRunner = void 0;
|
|
|
37
37
|
const worker_threads_1 = require("worker_threads");
|
|
38
38
|
const code_validator_1 = require("../validators/code-validator");
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
|
+
const DEFAULT_POOL_SIZE = 10;
|
|
41
|
+
const MAX_EXECUTIONS_PER_WORKER = 100;
|
|
42
|
+
const MAX_WORKER_AGE_MS = 10 * 60 * 1000; // 10 minutes
|
|
40
43
|
class NodeRunner {
|
|
41
44
|
constructor(options = {}) {
|
|
42
|
-
this.
|
|
45
|
+
this.pool = [];
|
|
46
|
+
this.waitQueue = [];
|
|
43
47
|
this.executionId = 0;
|
|
44
|
-
this.
|
|
48
|
+
this.isTerminating = false;
|
|
45
49
|
this.workerPath = path.join(__dirname, "../../../public/node-worker.js");
|
|
46
50
|
this.maxMemoryMB = options.maxMemoryMB || 128;
|
|
47
|
-
this.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
this.poolSize = options.poolSize || DEFAULT_POOL_SIZE;
|
|
52
|
+
this.maxExecutionsPerWorker =
|
|
53
|
+
options.maxExecutionsPerWorker || MAX_EXECUTIONS_PER_WORKER;
|
|
54
|
+
this.maxWorkerAgeMs = options.maxWorkerAgeMs || MAX_WORKER_AGE_MS;
|
|
55
|
+
// Pre-warm the pool
|
|
56
|
+
for (let i = 0; i < this.poolSize; i++) {
|
|
57
|
+
this.pool.push(this.createPooledWorker());
|
|
52
58
|
}
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
}
|
|
60
|
+
// ── Worker lifecycle ───────────────────────────────────
|
|
61
|
+
createPooledWorker() {
|
|
62
|
+
const worker = new worker_threads_1.Worker(this.workerPath, {
|
|
55
63
|
resourceLimits: {
|
|
56
64
|
maxOldGenerationSizeMb: this.maxMemoryMB,
|
|
57
65
|
maxYoungGenerationSizeMb: this.maxMemoryMB / 2,
|
|
58
66
|
codeRangeSizeMb: 16,
|
|
59
67
|
},
|
|
60
|
-
// Prevent worker from accessing parent environment variables
|
|
61
68
|
env: {},
|
|
62
69
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
result,
|
|
73
|
-
error,
|
|
74
|
-
logs,
|
|
75
|
-
executionTime,
|
|
76
|
-
validation: { isValid: true, errors: [], warnings: [] },
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
this.worker.on("error", (error) => {
|
|
81
|
-
console.error("Worker error:", error);
|
|
82
|
-
// Resolve all pending executions with error
|
|
83
|
-
this.pendingExecutions.forEach((pending, id) => {
|
|
84
|
-
clearTimeout(pending.timeout);
|
|
85
|
-
pending.resolve({
|
|
86
|
-
success: false,
|
|
87
|
-
timestamp: new Date().toISOString(),
|
|
88
|
-
error: {
|
|
89
|
-
message: error.message || "Worker crashed",
|
|
90
|
-
name: "WorkerError",
|
|
91
|
-
},
|
|
92
|
-
logs: [],
|
|
93
|
-
validation: { isValid: true, errors: [], warnings: [] },
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
this.pendingExecutions.clear();
|
|
97
|
-
this.initWorker();
|
|
70
|
+
const pw = {
|
|
71
|
+
worker,
|
|
72
|
+
busy: false,
|
|
73
|
+
executionCount: 0,
|
|
74
|
+
createdAt: Date.now(),
|
|
75
|
+
};
|
|
76
|
+
worker.on("error", (error) => {
|
|
77
|
+
console.error("[NodeRunner] Worker error:", error.message);
|
|
78
|
+
this.replaceWorker(pw);
|
|
98
79
|
});
|
|
99
|
-
|
|
100
|
-
if (code !== 0) {
|
|
101
|
-
console.error(`Worker
|
|
102
|
-
|
|
103
|
-
this.pendingExecutions.forEach((pending, id) => {
|
|
104
|
-
clearTimeout(pending.timeout);
|
|
105
|
-
pending.resolve({
|
|
106
|
-
success: false,
|
|
107
|
-
timestamp: new Date().toISOString(),
|
|
108
|
-
error: {
|
|
109
|
-
message: `Worker exited with code ${code}`,
|
|
110
|
-
name: "WorkerExitError",
|
|
111
|
-
},
|
|
112
|
-
logs: [],
|
|
113
|
-
validation: { isValid: true, errors: [], warnings: [] },
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
this.pendingExecutions.clear();
|
|
80
|
+
worker.on("exit", (code) => {
|
|
81
|
+
if (code !== 0 && !this.isTerminating) {
|
|
82
|
+
console.error(`[NodeRunner] Worker exited with code ${code}`);
|
|
83
|
+
this.replaceWorker(pw);
|
|
117
84
|
}
|
|
118
85
|
});
|
|
86
|
+
return pw;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Terminate old worker (frees its V8 isolate) and replace with a fresh one.
|
|
90
|
+
*/
|
|
91
|
+
replaceWorker(pw) {
|
|
92
|
+
if (this.isTerminating)
|
|
93
|
+
return;
|
|
94
|
+
const idx = this.pool.indexOf(pw);
|
|
95
|
+
if (idx === -1)
|
|
96
|
+
return;
|
|
97
|
+
pw.worker.removeAllListeners();
|
|
98
|
+
pw.worker.terminate().catch(() => { });
|
|
99
|
+
const newPw = this.createPooledWorker();
|
|
100
|
+
this.pool[idx] = newPw;
|
|
101
|
+
// Hand to next waiting caller if any
|
|
102
|
+
if (this.waitQueue.length > 0) {
|
|
103
|
+
const next = this.waitQueue.shift();
|
|
104
|
+
newPw.busy = true;
|
|
105
|
+
next(newPw);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
shouldRecycle(pw) {
|
|
109
|
+
return (pw.executionCount >= this.maxExecutionsPerWorker ||
|
|
110
|
+
Date.now() - pw.createdAt > this.maxWorkerAgeMs);
|
|
111
|
+
}
|
|
112
|
+
// ── Pool management ────────────────────────────────────
|
|
113
|
+
acquire() {
|
|
114
|
+
const idle = this.pool.find((pw) => !pw.busy);
|
|
115
|
+
if (idle) {
|
|
116
|
+
idle.busy = true;
|
|
117
|
+
return Promise.resolve(idle);
|
|
118
|
+
}
|
|
119
|
+
// All busy — wait in queue
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
this.waitQueue.push(resolve);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
release(pw) {
|
|
125
|
+
pw.busy = false;
|
|
126
|
+
pw.executionCount++;
|
|
127
|
+
// Recycle if stale — this is what prevents the slow memory creep
|
|
128
|
+
if (this.shouldRecycle(pw)) {
|
|
129
|
+
console.info(`[NodeRunner] Recycling worker (executions: ${pw.executionCount}, ` +
|
|
130
|
+
`age: ${((Date.now() - pw.createdAt) / 1000).toFixed(0)}s)`);
|
|
131
|
+
this.replaceWorker(pw);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Hand to next waiting caller
|
|
135
|
+
if (this.waitQueue.length > 0) {
|
|
136
|
+
const next = this.waitQueue.shift();
|
|
137
|
+
pw.busy = true;
|
|
138
|
+
next(pw);
|
|
139
|
+
}
|
|
119
140
|
}
|
|
141
|
+
// ── Execution ──────────────────────────────────────────
|
|
120
142
|
async execute(functionBody, schema, args) {
|
|
121
143
|
const validation = code_validator_1.CodeValidator.validate(functionBody, schema);
|
|
122
144
|
if (!validation.isValid || !validation.wrappedCode) {
|
|
@@ -131,11 +153,33 @@ class NodeRunner {
|
|
|
131
153
|
validation,
|
|
132
154
|
};
|
|
133
155
|
}
|
|
156
|
+
const pw = await this.acquire();
|
|
134
157
|
return new Promise((resolve) => {
|
|
135
158
|
const id = ++this.executionId;
|
|
136
159
|
const timeout = schema.timeout || 5000;
|
|
160
|
+
const cleanup = () => {
|
|
161
|
+
pw.worker.removeListener("message", onMessage);
|
|
162
|
+
clearTimeout(timeoutId);
|
|
163
|
+
};
|
|
164
|
+
const onMessage = (message) => {
|
|
165
|
+
if (message.id !== id)
|
|
166
|
+
return;
|
|
167
|
+
cleanup();
|
|
168
|
+
this.release(pw);
|
|
169
|
+
resolve({
|
|
170
|
+
timestamp: new Date().toISOString(),
|
|
171
|
+
success: message.success,
|
|
172
|
+
result: message.result,
|
|
173
|
+
error: message.error,
|
|
174
|
+
logs: message.logs,
|
|
175
|
+
executionTime: message.executionTime,
|
|
176
|
+
validation,
|
|
177
|
+
});
|
|
178
|
+
};
|
|
137
179
|
const timeoutId = setTimeout(() => {
|
|
138
|
-
|
|
180
|
+
cleanup();
|
|
181
|
+
// Timeout: kill THIS worker and replace it (bounded — pool stays fixed size)
|
|
182
|
+
this.replaceWorker(pw);
|
|
139
183
|
resolve({
|
|
140
184
|
success: false,
|
|
141
185
|
timestamp: new Date().toISOString(),
|
|
@@ -146,17 +190,9 @@ class NodeRunner {
|
|
|
146
190
|
logs: [],
|
|
147
191
|
validation,
|
|
148
192
|
});
|
|
149
|
-
// Restart worker to kill the hanging execution
|
|
150
|
-
this.initWorker();
|
|
151
193
|
}, timeout);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
resolve({ ...result, validation });
|
|
155
|
-
},
|
|
156
|
-
timeout: timeoutId,
|
|
157
|
-
});
|
|
158
|
-
// Send code to worker for execution
|
|
159
|
-
this.worker?.postMessage({
|
|
194
|
+
pw.worker.on("message", onMessage);
|
|
195
|
+
pw.worker.postMessage({
|
|
160
196
|
id,
|
|
161
197
|
code: validation.wrappedCode,
|
|
162
198
|
functionName: schema.name,
|
|
@@ -165,25 +201,29 @@ class NodeRunner {
|
|
|
165
201
|
});
|
|
166
202
|
});
|
|
167
203
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
this.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
204
|
+
// ── Cleanup ────────────────────────────────────────────
|
|
205
|
+
async terminate() {
|
|
206
|
+
this.isTerminating = true;
|
|
207
|
+
this.waitQueue = [];
|
|
208
|
+
await Promise.all(this.pool.map((pw) => {
|
|
209
|
+
pw.worker.removeAllListeners();
|
|
210
|
+
return pw.worker.terminate().catch(() => { });
|
|
211
|
+
}));
|
|
212
|
+
this.pool = [];
|
|
213
|
+
}
|
|
214
|
+
// ── Diagnostics (expose via /metrics) ──────────────────
|
|
215
|
+
getStats() {
|
|
216
|
+
return {
|
|
217
|
+
poolSize: this.pool.length,
|
|
218
|
+
busy: this.pool.filter((pw) => pw.busy).length,
|
|
219
|
+
idle: this.pool.filter((pw) => !pw.busy).length,
|
|
220
|
+
waitingInQueue: this.waitQueue.length,
|
|
221
|
+
workers: this.pool.map((pw) => ({
|
|
222
|
+
busy: pw.busy,
|
|
223
|
+
executionCount: pw.executionCount,
|
|
224
|
+
ageSeconds: ((Date.now() - pw.createdAt) / 1000).toFixed(0),
|
|
225
|
+
})),
|
|
226
|
+
};
|
|
187
227
|
}
|
|
188
228
|
}
|
|
189
229
|
exports.NodeRunner = NodeRunner;
|