@ondc/automation-mock-runner 1.3.17 → 1.3.19
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.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { Logger } from "./utils/logger";
|
|
|
4
4
|
import { ExecutionResult } from "./types/execution-results";
|
|
5
5
|
export declare class MockRunner {
|
|
6
6
|
private config;
|
|
7
|
-
private
|
|
7
|
+
private static sharedRunner;
|
|
8
8
|
logger: Logger;
|
|
9
9
|
constructor(config: MockPlaygroundConfigType, skipValidation?: boolean);
|
|
10
10
|
getRunnerInstance(): BaseCodeRunner;
|
package/dist/lib/MockRunner.js
CHANGED
|
@@ -38,11 +38,12 @@ class MockRunner {
|
|
|
38
38
|
}
|
|
39
39
|
getRunnerInstance() {
|
|
40
40
|
this.logger.debug("Getting code runner instance");
|
|
41
|
-
if (!
|
|
42
|
-
|
|
41
|
+
if (!MockRunner.sharedRunner) {
|
|
42
|
+
MockRunner.sharedRunner = runner_factory_1.RunnerFactory.createRunner({}, this.logger);
|
|
43
43
|
}
|
|
44
|
-
this.logger.debug("Code runner instance obtained successfully: " +
|
|
45
|
-
|
|
44
|
+
this.logger.debug("Code runner instance obtained successfully: " +
|
|
45
|
+
MockRunner.sharedRunner.toString());
|
|
46
|
+
return MockRunner.sharedRunner;
|
|
46
47
|
}
|
|
47
48
|
getConfig() {
|
|
48
49
|
return this.config;
|
|
@@ -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;
|
|
@@ -43,6 +43,31 @@ describe("MockRunner", () => {
|
|
|
43
43
|
expect(mockRunner).toBeDefined();
|
|
44
44
|
expect(mockRunner).toBeInstanceOf(MockRunner_1.MockRunner);
|
|
45
45
|
});
|
|
46
|
+
it("should reuse the same BaseCodeRunner across MockRunner instances", async () => {
|
|
47
|
+
const baseConfig = {
|
|
48
|
+
meta: {
|
|
49
|
+
domain: "ONDC:TRV14",
|
|
50
|
+
version: "2.0.0",
|
|
51
|
+
flowId: "singleton-test",
|
|
52
|
+
},
|
|
53
|
+
transaction_data: {
|
|
54
|
+
transaction_id: "11111111-1111-1111-1111-111111111111",
|
|
55
|
+
latest_timestamp: "1970-01-01T00:00:00.000Z",
|
|
56
|
+
},
|
|
57
|
+
steps: [],
|
|
58
|
+
transaction_history: [],
|
|
59
|
+
validationLib: "",
|
|
60
|
+
helperLib: "",
|
|
61
|
+
};
|
|
62
|
+
const firstRunner = new MockRunner_1.MockRunner(baseConfig, true);
|
|
63
|
+
firstRunner
|
|
64
|
+
.getConfig()
|
|
65
|
+
.steps.push(firstRunner.getDefaultStep("search", "search_0"));
|
|
66
|
+
const optimizedConfig = await (0, configHelper_1.createOptimizedMockConfig)(firstRunner.getConfig());
|
|
67
|
+
const mockRunnerA = new MockRunner_1.MockRunner(optimizedConfig, true);
|
|
68
|
+
const mockRunnerB = new MockRunner_1.MockRunner(optimizedConfig, true);
|
|
69
|
+
expect(mockRunnerA.getRunnerInstance()).toBe(mockRunnerB.getRunnerInstance());
|
|
70
|
+
});
|
|
46
71
|
});
|
|
47
72
|
describe("Config Validation", () => {
|
|
48
73
|
it("should validate correct config successfully", () => {
|