@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.
@@ -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 worker;
5
+ private pool;
6
+ private waitQueue;
6
7
  private executionId;
7
- private pendingExecutions;
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 initWorker;
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.worker = null;
45
+ this.pool = [];
46
+ this.waitQueue = [];
43
47
  this.executionId = 0;
44
- this.pendingExecutions = new Map();
48
+ this.isTerminating = false;
45
49
  this.workerPath = path.join(__dirname, "../../../public/node-worker.js");
46
50
  this.maxMemoryMB = options.maxMemoryMB || 128;
47
- this.initWorker();
48
- }
49
- initWorker() {
50
- if (this.worker) {
51
- this.worker.terminate();
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
- // Create worker with resource limits
54
- this.worker = new worker_threads_1.Worker(this.workerPath, {
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
- this.worker.on("message", (message) => {
64
- const { id, success, result, error, logs, executionTime } = message;
65
- const pending = this.pendingExecutions.get(id);
66
- if (pending) {
67
- clearTimeout(pending.timeout);
68
- this.pendingExecutions.delete(id);
69
- pending.resolve({
70
- timestamp: new Date().toISOString(),
71
- success,
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
- this.worker.on("exit", (code) => {
100
- if (code !== 0) {
101
- console.error(`Worker stopped with exit code ${code}`);
102
- // Handle any pending executions
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
- this.pendingExecutions.delete(id);
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
- this.pendingExecutions.set(id, {
153
- resolve: (result) => {
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
- terminate() {
169
- // Clear all pending executions
170
- this.pendingExecutions.forEach((pending) => {
171
- clearTimeout(pending.timeout);
172
- pending.resolve({
173
- success: false,
174
- timestamp: new Date().toISOString(),
175
- error: {
176
- message: "Runner terminated",
177
- name: "TerminationError",
178
- },
179
- logs: [],
180
- validation: { isValid: true, errors: [], warnings: [] },
181
- });
182
- });
183
- this.pendingExecutions.clear();
184
- // Terminate worker
185
- this.worker?.terminate();
186
- this.worker = null;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ondc/automation-mock-runner",
3
- "version": "1.3.16",
3
+ "version": "1.3.18",
4
4
  "description": "A TypeScript library for ONDC automation mock runner",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",