tywrap 0.1.2 → 0.2.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/README.md +11 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime/base.d.ts +17 -7
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +18 -1
- package/dist/runtime/base.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +252 -0
- package/dist/runtime/bounded-context.d.ts.map +1 -0
- package/dist/runtime/bounded-context.js +454 -0
- package/dist/runtime/bounded-context.js.map +1 -0
- package/dist/runtime/bridge-protocol.d.ts +167 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -0
- package/dist/runtime/bridge-protocol.js +247 -0
- package/dist/runtime/bridge-protocol.js.map +1 -0
- package/dist/runtime/disposable.d.ts +40 -0
- package/dist/runtime/disposable.d.ts.map +1 -0
- package/dist/runtime/disposable.js +49 -0
- package/dist/runtime/disposable.js.map +1 -0
- package/dist/runtime/http-io.d.ts +91 -0
- package/dist/runtime/http-io.d.ts.map +1 -0
- package/dist/runtime/http-io.js +195 -0
- package/dist/runtime/http-io.js.map +1 -0
- package/dist/runtime/http.d.ts +47 -13
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +55 -74
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts +142 -28
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +321 -168
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +19 -125
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +19 -550
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +131 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -0
- package/dist/runtime/pooled-transport.js +175 -0
- package/dist/runtime/pooled-transport.js.map +1 -0
- package/dist/runtime/process-io.d.ts +204 -0
- package/dist/runtime/process-io.d.ts.map +1 -0
- package/dist/runtime/process-io.js +695 -0
- package/dist/runtime/process-io.js.map +1 -0
- package/dist/runtime/pyodide-io.d.ts +155 -0
- package/dist/runtime/pyodide-io.d.ts.map +1 -0
- package/dist/runtime/pyodide-io.js +397 -0
- package/dist/runtime/pyodide-io.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +51 -19
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +57 -186
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +81 -0
- package/dist/runtime/safe-codec.d.ts.map +1 -0
- package/dist/runtime/safe-codec.js +345 -0
- package/dist/runtime/safe-codec.js.map +1 -0
- package/dist/runtime/transport.d.ts +186 -0
- package/dist/runtime/transport.d.ts.map +1 -0
- package/dist/runtime/transport.js +86 -0
- package/dist/runtime/transport.js.map +1 -0
- package/dist/runtime/validators.d.ts +131 -0
- package/dist/runtime/validators.d.ts.map +1 -0
- package/dist/runtime/validators.js +219 -0
- package/dist/runtime/validators.js.map +1 -0
- package/dist/runtime/worker-pool.d.ts +196 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -0
- package/dist/runtime/worker-pool.js +371 -0
- package/dist/runtime/worker-pool.js.map +1 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +120 -1
- package/dist/utils/codec.js.map +1 -1
- package/package.json +2 -2
- package/runtime/python_bridge.py +30 -3
- package/runtime/safe_codec.py +344 -0
- package/src/index.ts +48 -5
- package/src/runtime/base.ts +18 -26
- package/src/runtime/bounded-context.ts +608 -0
- package/src/runtime/bridge-protocol.ts +319 -0
- package/src/runtime/disposable.ts +65 -0
- package/src/runtime/http-io.ts +244 -0
- package/src/runtime/http.ts +71 -117
- package/src/runtime/node.ts +460 -217
- package/src/runtime/optimized-node.ts +19 -761
- package/src/runtime/pooled-transport.ts +252 -0
- package/src/runtime/process-io.ts +902 -0
- package/src/runtime/pyodide-io.ts +485 -0
- package/src/runtime/pyodide.ts +75 -215
- package/src/runtime/safe-codec.ts +443 -0
- package/src/runtime/transport.ts +273 -0
- package/src/runtime/validators.ts +241 -0
- package/src/runtime/worker-pool.ts +498 -0
- package/src/utils/codec.ts +126 -1
|
@@ -1,763 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* @deprecated Import from './node.js' instead.
|
|
3
|
+
*
|
|
4
|
+
* OptimizedNodeBridge has been unified with NodeBridge. The NodeBridge class
|
|
5
|
+
* now supports both single-process mode (default) and multi-process pooling
|
|
6
|
+
* via the minProcesses/maxProcesses options.
|
|
7
|
+
*
|
|
8
|
+
* Migration:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // Before
|
|
11
|
+
* import { OptimizedNodeBridge, ProcessPoolOptions } from './optimized-node.js';
|
|
12
|
+
* const bridge = new OptimizedNodeBridge({ minProcesses: 2, maxProcesses: 4 });
|
|
13
|
+
*
|
|
14
|
+
* // After
|
|
15
|
+
* import { NodeBridge, NodeBridgeOptions } from './node.js';
|
|
16
|
+
* const bridge = new NodeBridge({ minProcesses: 2, maxProcesses: 4 });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* This file is maintained for backward compatibility only.
|
|
4
20
|
*/
|
|
5
|
-
|
|
6
|
-
import { delimiter, isAbsolute, join, resolve } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { createRequire } from 'node:module';
|
|
9
|
-
import type { ChildProcess } from 'child_process';
|
|
10
|
-
import { EventEmitter } from 'events';
|
|
11
|
-
|
|
12
|
-
import { globalCache } from '../utils/cache.js';
|
|
13
|
-
import { autoRegisterArrowDecoder, decodeValueAsync } from '../utils/codec.js';
|
|
14
|
-
import { getDefaultPythonPath } from '../utils/python.js';
|
|
15
|
-
import { getVenvBinDir, getVenvPythonExe } from '../utils/runtime.js';
|
|
16
|
-
|
|
17
|
-
import { RuntimeBridge } from './base.js';
|
|
18
|
-
import { BridgeProtocolError } from './errors.js';
|
|
19
|
-
import {
|
|
20
|
-
BridgeCore,
|
|
21
|
-
type RpcRequest,
|
|
22
|
-
ensureJsonFallback,
|
|
23
|
-
ensurePythonEncoding,
|
|
24
|
-
getMaxLineLengthFromEnv,
|
|
25
|
-
getPathKey,
|
|
26
|
-
normalizeEnv,
|
|
27
|
-
} from './bridge-core.js';
|
|
28
|
-
import { getComponentLogger } from '../utils/logger.js';
|
|
29
|
-
|
|
30
|
-
const log = getComponentLogger('OptimizedBridge');
|
|
31
|
-
|
|
32
|
-
interface ProcessPoolOptions {
|
|
33
|
-
minProcesses?: number;
|
|
34
|
-
maxProcesses?: number;
|
|
35
|
-
maxIdleTime?: number; // ms to keep idle processes alive
|
|
36
|
-
maxRequestsPerProcess?: number; // restart process after N requests
|
|
37
|
-
pythonPath?: string;
|
|
38
|
-
scriptPath?: string;
|
|
39
|
-
virtualEnv?: string | undefined;
|
|
40
|
-
cwd?: string;
|
|
41
|
-
timeoutMs?: number;
|
|
42
|
-
maxLineLength?: number;
|
|
43
|
-
enableJsonFallback?: boolean;
|
|
44
|
-
enableCache?: boolean;
|
|
45
|
-
env?: Record<string, string | undefined>;
|
|
46
|
-
warmupCommands?: Array<{ method: string; params: unknown }>; // Commands to warm up processes
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface ResolvedProcessPoolOptions {
|
|
50
|
-
minProcesses: number;
|
|
51
|
-
maxProcesses: number;
|
|
52
|
-
maxIdleTime: number;
|
|
53
|
-
maxRequestsPerProcess: number;
|
|
54
|
-
pythonPath: string;
|
|
55
|
-
scriptPath: string;
|
|
56
|
-
virtualEnv?: string;
|
|
57
|
-
cwd: string;
|
|
58
|
-
timeoutMs: number;
|
|
59
|
-
maxLineLength?: number;
|
|
60
|
-
enableJsonFallback: boolean;
|
|
61
|
-
enableCache: boolean;
|
|
62
|
-
env: Record<string, string | undefined>;
|
|
63
|
-
warmupCommands: Array<{ method: string; params: unknown }>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface WorkerProcess {
|
|
67
|
-
process: ChildProcess;
|
|
68
|
-
id: string;
|
|
69
|
-
requestCount: number;
|
|
70
|
-
lastUsed: number;
|
|
71
|
-
busy: boolean;
|
|
72
|
-
quarantined: boolean;
|
|
73
|
-
core: BridgeCore;
|
|
74
|
-
stats: {
|
|
75
|
-
totalRequests: number;
|
|
76
|
-
totalTime: number;
|
|
77
|
-
averageTime: number;
|
|
78
|
-
errorCount: number;
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
interface OptimizedBridgeStats {
|
|
83
|
-
totalRequests: number;
|
|
84
|
-
totalTime: number;
|
|
85
|
-
cacheHits: number;
|
|
86
|
-
poolHits: number;
|
|
87
|
-
poolMisses: number;
|
|
88
|
-
processSpawns: number;
|
|
89
|
-
processDeaths: number;
|
|
90
|
-
memoryPeak: number;
|
|
91
|
-
averageTime: number;
|
|
92
|
-
cacheHitRate: number;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
interface OptimizedBridgeStatsSnapshot extends OptimizedBridgeStats {
|
|
96
|
-
poolSize: number;
|
|
97
|
-
busyWorkers: number;
|
|
98
|
-
memoryUsage: NodeJS.MemoryUsage;
|
|
99
|
-
workerStats: Array<{
|
|
100
|
-
id: string;
|
|
101
|
-
requestCount: number;
|
|
102
|
-
averageTime: number;
|
|
103
|
-
errorCount: number;
|
|
104
|
-
busy: boolean;
|
|
105
|
-
pendingRequests: number;
|
|
106
|
-
}>;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function resolveDefaultScriptPath(): string {
|
|
110
|
-
try {
|
|
111
|
-
return fileURLToPath(new URL('../../runtime/python_bridge.py', import.meta.url));
|
|
112
|
-
} catch {
|
|
113
|
-
return 'runtime/python_bridge.py';
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function resolveVirtualEnv(
|
|
118
|
-
virtualEnv: string,
|
|
119
|
-
cwd: string
|
|
120
|
-
): {
|
|
121
|
-
venvPath: string;
|
|
122
|
-
binDir: string;
|
|
123
|
-
pythonPath: string;
|
|
124
|
-
} {
|
|
125
|
-
const venvPath = resolve(cwd, virtualEnv);
|
|
126
|
-
const binDir = join(venvPath, getVenvBinDir());
|
|
127
|
-
const pythonPath = join(binDir, getVenvPythonExe());
|
|
128
|
-
return { venvPath, binDir, pythonPath };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export class OptimizedNodeBridge extends RuntimeBridge {
|
|
132
|
-
private processPool: WorkerProcess[] = [];
|
|
133
|
-
private roundRobinIndex = 0;
|
|
134
|
-
private cleanupTimer?: NodeJS.Timeout;
|
|
135
|
-
private options: ResolvedProcessPoolOptions;
|
|
136
|
-
private emitter = new EventEmitter();
|
|
137
|
-
private disposed = false;
|
|
138
|
-
|
|
139
|
-
// Performance monitoring
|
|
140
|
-
private stats: OptimizedBridgeStats = {
|
|
141
|
-
totalRequests: 0,
|
|
142
|
-
totalTime: 0,
|
|
143
|
-
cacheHits: 0,
|
|
144
|
-
poolHits: 0,
|
|
145
|
-
poolMisses: 0,
|
|
146
|
-
processSpawns: 0,
|
|
147
|
-
processDeaths: 0,
|
|
148
|
-
memoryPeak: 0,
|
|
149
|
-
averageTime: 0,
|
|
150
|
-
cacheHitRate: 0,
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
constructor(options: ProcessPoolOptions = {}) {
|
|
154
|
-
super();
|
|
155
|
-
const cwd = options.cwd ?? process.cwd();
|
|
156
|
-
const virtualEnv = options.virtualEnv ? resolve(cwd, options.virtualEnv) : undefined;
|
|
157
|
-
const venv = virtualEnv ? resolveVirtualEnv(virtualEnv, cwd) : undefined;
|
|
158
|
-
const scriptPath = options.scriptPath ?? resolveDefaultScriptPath();
|
|
159
|
-
const resolvedScriptPath = isAbsolute(scriptPath) ? scriptPath : resolve(cwd, scriptPath);
|
|
160
|
-
this.options = {
|
|
161
|
-
minProcesses: options.minProcesses ?? 2,
|
|
162
|
-
maxProcesses: options.maxProcesses ?? 8,
|
|
163
|
-
maxIdleTime: options.maxIdleTime ?? 300000, // 5 minutes
|
|
164
|
-
maxRequestsPerProcess: options.maxRequestsPerProcess ?? 1000,
|
|
165
|
-
pythonPath: options.pythonPath ?? venv?.pythonPath ?? getDefaultPythonPath(),
|
|
166
|
-
scriptPath: resolvedScriptPath,
|
|
167
|
-
virtualEnv,
|
|
168
|
-
cwd,
|
|
169
|
-
timeoutMs: options.timeoutMs ?? 30000,
|
|
170
|
-
maxLineLength: options.maxLineLength,
|
|
171
|
-
enableJsonFallback: options.enableJsonFallback ?? false,
|
|
172
|
-
enableCache: options.enableCache ?? false,
|
|
173
|
-
env: options.env ?? {},
|
|
174
|
-
warmupCommands: options.warmupCommands ?? [],
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// Start with minimum processes
|
|
178
|
-
this.startCleanupScheduler();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async init(): Promise<void> {
|
|
182
|
-
if (this.disposed) {
|
|
183
|
-
throw new Error('Bridge has been disposed');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const require = createRequire(import.meta.url);
|
|
187
|
-
await autoRegisterArrowDecoder({
|
|
188
|
-
loader: () => require('apache-arrow'),
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// Ensure minimum processes are available
|
|
192
|
-
while (this.processPool.length < this.options.minProcesses) {
|
|
193
|
-
await this.spawnProcess();
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Warm up processes if configured
|
|
197
|
-
if (this.options.warmupCommands.length > 0) {
|
|
198
|
-
await this.warmupProcesses();
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async call<T = unknown>(
|
|
203
|
-
module: string,
|
|
204
|
-
functionName: string,
|
|
205
|
-
args: unknown[],
|
|
206
|
-
kwargs?: Record<string, unknown>
|
|
207
|
-
): Promise<T> {
|
|
208
|
-
const startTime = performance.now();
|
|
209
|
-
|
|
210
|
-
const cacheKey = this.options.enableCache
|
|
211
|
-
? this.safeCacheKey('runtime_call', module, functionName, args, kwargs)
|
|
212
|
-
: null;
|
|
213
|
-
if (cacheKey) {
|
|
214
|
-
const cached = await globalCache.get<T>(cacheKey);
|
|
215
|
-
if (cached !== null) {
|
|
216
|
-
this.stats.cacheHits++;
|
|
217
|
-
this.updateStats(performance.now() - startTime);
|
|
218
|
-
// Runtime cache HIT for ${module}.${functionName}
|
|
219
|
-
return cached;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
const result = await this.executeRequest<T>({
|
|
225
|
-
method: 'call',
|
|
226
|
-
params: { module, functionName, args, kwargs },
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const duration = performance.now() - startTime;
|
|
230
|
-
|
|
231
|
-
// Cache result for pure functions (simple heuristic)
|
|
232
|
-
if (cacheKey && this.isPureFunctionCandidate(functionName, args)) {
|
|
233
|
-
await globalCache.set(cacheKey, result, {
|
|
234
|
-
computeTime: duration,
|
|
235
|
-
dependencies: [module],
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
this.updateStats(duration);
|
|
240
|
-
return result;
|
|
241
|
-
} catch (error) {
|
|
242
|
-
this.updateStats(performance.now() - startTime, true);
|
|
243
|
-
throw error;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async instantiate<T = unknown>(
|
|
248
|
-
module: string,
|
|
249
|
-
className: string,
|
|
250
|
-
args: unknown[],
|
|
251
|
-
kwargs?: Record<string, unknown>
|
|
252
|
-
): Promise<T> {
|
|
253
|
-
const startTime = performance.now();
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
const result = await this.executeRequest<T>({
|
|
257
|
-
method: 'instantiate',
|
|
258
|
-
params: { module, className, args, kwargs },
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
this.updateStats(performance.now() - startTime);
|
|
262
|
-
return result;
|
|
263
|
-
} catch (error) {
|
|
264
|
-
this.updateStats(performance.now() - startTime, true);
|
|
265
|
-
throw error;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async callMethod<T = unknown>(
|
|
270
|
-
handle: string,
|
|
271
|
-
methodName: string,
|
|
272
|
-
args: unknown[],
|
|
273
|
-
kwargs?: Record<string, unknown>
|
|
274
|
-
): Promise<T> {
|
|
275
|
-
const startTime = performance.now();
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
const result = await this.executeRequest<T>({
|
|
279
|
-
method: 'call_method',
|
|
280
|
-
params: { handle, methodName, args, kwargs },
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
this.updateStats(performance.now() - startTime);
|
|
284
|
-
return result;
|
|
285
|
-
} catch (error) {
|
|
286
|
-
this.updateStats(performance.now() - startTime, true);
|
|
287
|
-
throw error;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async disposeInstance(handle: string): Promise<void> {
|
|
292
|
-
await this.executeRequest<void>({
|
|
293
|
-
method: 'dispose_instance',
|
|
294
|
-
params: { handle },
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Execute request with intelligent process selection
|
|
300
|
-
*/
|
|
301
|
-
private async executeRequest<T>(payload: Omit<RpcRequest, 'id' | 'protocol'>): Promise<T> {
|
|
302
|
-
let worker = this.selectOptimalWorker();
|
|
303
|
-
|
|
304
|
-
// Spawn new process if none available and under limit
|
|
305
|
-
if (!worker && this.processPool.length < this.options.maxProcesses) {
|
|
306
|
-
try {
|
|
307
|
-
worker = await this.spawnProcess();
|
|
308
|
-
this.stats.poolMisses++;
|
|
309
|
-
} catch (error) {
|
|
310
|
-
throw new Error(`Failed to spawn worker process: ${error}`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Wait for worker if all are busy
|
|
315
|
-
worker ??= await this.waitForAvailableWorker();
|
|
316
|
-
|
|
317
|
-
this.stats.poolHits++;
|
|
318
|
-
return this.sendToWorker<T>(worker, payload);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Select optimal worker based on load and performance
|
|
323
|
-
*/
|
|
324
|
-
private selectOptimalWorker(): WorkerProcess | null {
|
|
325
|
-
const availableWorkers = this.processPool.filter(
|
|
326
|
-
w => !w.busy && !w.quarantined && w.process.exitCode === null
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
if (availableWorkers.length === 0) {
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Simple round-robin for now, could be enhanced with load-based selection
|
|
334
|
-
const worker = availableWorkers[this.roundRobinIndex % availableWorkers.length];
|
|
335
|
-
this.roundRobinIndex = (this.roundRobinIndex + 1) % availableWorkers.length;
|
|
336
|
-
|
|
337
|
-
return worker ?? null;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Wait for any worker to become available
|
|
342
|
-
*/
|
|
343
|
-
private async waitForAvailableWorker(timeoutMs: number = 5000): Promise<WorkerProcess> {
|
|
344
|
-
return new Promise((resolvePromise, reject) => {
|
|
345
|
-
const timeout = setTimeout(() => {
|
|
346
|
-
reject(new Error('Timeout waiting for available worker'));
|
|
347
|
-
}, timeoutMs);
|
|
348
|
-
|
|
349
|
-
const checkWorker = (): void => {
|
|
350
|
-
const worker = this.selectOptimalWorker();
|
|
351
|
-
if (worker) {
|
|
352
|
-
clearTimeout(timeout);
|
|
353
|
-
resolvePromise(worker);
|
|
354
|
-
} else {
|
|
355
|
-
// Check again in 10ms
|
|
356
|
-
setTimeout(checkWorker, 10);
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
checkWorker();
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Send request to specific worker
|
|
366
|
-
*/
|
|
367
|
-
private async sendToWorker<T>(
|
|
368
|
-
worker: WorkerProcess,
|
|
369
|
-
payload: Omit<RpcRequest, 'id' | 'protocol'>
|
|
370
|
-
): Promise<T> {
|
|
371
|
-
worker.busy = true;
|
|
372
|
-
worker.requestCount++;
|
|
373
|
-
worker.lastUsed = Date.now();
|
|
374
|
-
|
|
375
|
-
const startTime = performance.now();
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
const result = await worker.core.send<T>(payload);
|
|
379
|
-
const duration = performance.now() - startTime;
|
|
380
|
-
worker.stats.totalTime += duration;
|
|
381
|
-
worker.stats.totalRequests++;
|
|
382
|
-
worker.stats.averageTime = worker.stats.totalTime / worker.stats.totalRequests;
|
|
383
|
-
return result;
|
|
384
|
-
} catch (error) {
|
|
385
|
-
worker.stats.errorCount++;
|
|
386
|
-
throw error;
|
|
387
|
-
} finally {
|
|
388
|
-
worker.busy = false;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
private quarantineWorker(worker: WorkerProcess, error: Error): void {
|
|
393
|
-
if (worker.quarantined) {
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
worker.quarantined = true;
|
|
397
|
-
log.warn('Quarantining worker', { workerId: worker.id, error: String(error) });
|
|
398
|
-
this.terminateWorker(worker, { force: true })
|
|
399
|
-
.then(() => {
|
|
400
|
-
if (!this.disposed && this.processPool.length < this.options.minProcesses) {
|
|
401
|
-
this.spawnProcess().catch(spawnError => {
|
|
402
|
-
log.error('Failed to spawn replacement worker after quarantine', {
|
|
403
|
-
error: String(spawnError),
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
})
|
|
408
|
-
.catch(terminateError => {
|
|
409
|
-
log.warn('Failed to terminate quarantined worker', {
|
|
410
|
-
workerId: worker.id,
|
|
411
|
-
error: String(terminateError),
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Spawn new worker process with optimizations
|
|
418
|
-
*/
|
|
419
|
-
private async spawnProcess(): Promise<WorkerProcess> {
|
|
420
|
-
const { spawn } = await import('child_process');
|
|
421
|
-
|
|
422
|
-
const workerId = `worker_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
423
|
-
|
|
424
|
-
let env = normalizeEnv(process.env as Record<string, string | undefined>, this.options.env);
|
|
425
|
-
env.PYTHONUNBUFFERED = '1'; // Ensure immediate output
|
|
426
|
-
env.PYTHONDONTWRITEBYTECODE = '1'; // Skip .pyc files for faster startup
|
|
427
|
-
ensurePythonEncoding(env);
|
|
428
|
-
if (this.options.virtualEnv) {
|
|
429
|
-
const venv = resolveVirtualEnv(this.options.virtualEnv, this.options.cwd);
|
|
430
|
-
env.VIRTUAL_ENV = venv.venvPath;
|
|
431
|
-
const pathKey = getPathKey(env);
|
|
432
|
-
// eslint-disable-next-line security/detect-object-injection -- env keys are dynamic by design
|
|
433
|
-
const currentPath = env[pathKey] ?? '';
|
|
434
|
-
// eslint-disable-next-line security/detect-object-injection -- env keys are dynamic by design
|
|
435
|
-
env[pathKey] = `${venv.binDir}${delimiter}${currentPath}`;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
ensureJsonFallback(env, this.options.enableJsonFallback);
|
|
439
|
-
|
|
440
|
-
env = normalizeEnv(env, {});
|
|
441
|
-
const maxLineLength = this.options.maxLineLength ?? getMaxLineLengthFromEnv(env);
|
|
442
|
-
|
|
443
|
-
const childProcess = spawn(this.options.pythonPath, [this.options.scriptPath], {
|
|
444
|
-
cwd: this.options.cwd,
|
|
445
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
446
|
-
env,
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
const worker: WorkerProcess = {
|
|
450
|
-
process: childProcess,
|
|
451
|
-
id: workerId,
|
|
452
|
-
requestCount: 0,
|
|
453
|
-
lastUsed: Date.now(),
|
|
454
|
-
busy: false,
|
|
455
|
-
quarantined: false,
|
|
456
|
-
core: null as unknown as BridgeCore,
|
|
457
|
-
stats: {
|
|
458
|
-
totalRequests: 0,
|
|
459
|
-
totalTime: 0,
|
|
460
|
-
averageTime: 0,
|
|
461
|
-
errorCount: 0,
|
|
462
|
-
},
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
worker.core = new BridgeCore(
|
|
466
|
-
{
|
|
467
|
-
write: (data: string): void => {
|
|
468
|
-
if (!worker.process.stdin?.writable) {
|
|
469
|
-
throw new BridgeProtocolError('Worker process stdin not writable');
|
|
470
|
-
}
|
|
471
|
-
worker.process.stdin.write(data);
|
|
472
|
-
},
|
|
473
|
-
},
|
|
474
|
-
{
|
|
475
|
-
timeoutMs: this.options.timeoutMs,
|
|
476
|
-
maxLineLength,
|
|
477
|
-
decodeValue: decodeValueAsync,
|
|
478
|
-
onFatalError: (error: Error): void => this.quarantineWorker(worker, error),
|
|
479
|
-
onTimeout: (error: Error): void => this.quarantineWorker(worker, error),
|
|
480
|
-
}
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
// Setup process event handlers
|
|
484
|
-
this.setupProcessHandlers(worker);
|
|
485
|
-
|
|
486
|
-
this.processPool.push(worker);
|
|
487
|
-
this.stats.processSpawns++;
|
|
488
|
-
|
|
489
|
-
// Spawned Python worker process ${workerId} (pool size: ${this.processPool.length})
|
|
490
|
-
|
|
491
|
-
return worker;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Setup event handlers for worker process
|
|
496
|
-
*/
|
|
497
|
-
private setupProcessHandlers(worker: WorkerProcess): void {
|
|
498
|
-
const childProcess = worker.process;
|
|
499
|
-
|
|
500
|
-
childProcess.stdout?.on('data', (chunk: Buffer) => {
|
|
501
|
-
worker.core.handleStdoutData(chunk);
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
childProcess.stderr?.on('data', (chunk: Buffer) => {
|
|
505
|
-
worker.core.handleStderrData(chunk);
|
|
506
|
-
const errorText = chunk.toString().trim();
|
|
507
|
-
if (errorText) {
|
|
508
|
-
log.warn('Worker stderr', { workerId: worker.id, output: errorText });
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
// Handle process exit
|
|
513
|
-
childProcess.on('exit', code => {
|
|
514
|
-
log.warn('Worker exited', { workerId: worker.id, code });
|
|
515
|
-
worker.core.handleProcessExit();
|
|
516
|
-
this.handleWorkerExit(worker, code);
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// Handle process errors
|
|
520
|
-
childProcess.on('error', error => {
|
|
521
|
-
log.error('Worker error', { workerId: worker.id, error: String(error) });
|
|
522
|
-
worker.core.handleProcessError(error);
|
|
523
|
-
this.handleWorkerExit(worker, -1);
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Handle worker process exit
|
|
529
|
-
*/
|
|
530
|
-
private handleWorkerExit(worker: WorkerProcess, _code: number | null): void {
|
|
531
|
-
if (!this.processPool.includes(worker)) {
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
worker.core.clear();
|
|
536
|
-
|
|
537
|
-
// Remove from pool
|
|
538
|
-
const index = this.processPool.indexOf(worker);
|
|
539
|
-
if (index >= 0) {
|
|
540
|
-
this.processPool.splice(index, 1);
|
|
541
|
-
this.stats.processDeaths++;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Spawn replacement if needed and not disposing
|
|
545
|
-
if (!this.disposed && this.processPool.length < this.options.minProcesses) {
|
|
546
|
-
this.spawnProcess().catch(error => {
|
|
547
|
-
log.error('Failed to spawn replacement worker', { error: String(error) });
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* Warm up processes with configured commands
|
|
554
|
-
*/
|
|
555
|
-
private async warmupProcesses(): Promise<void> {
|
|
556
|
-
const warmupPromises = this.processPool.map(async worker => {
|
|
557
|
-
for (const cmd of this.options.warmupCommands) {
|
|
558
|
-
try {
|
|
559
|
-
await this.sendToWorker(worker, {
|
|
560
|
-
method: cmd.method as 'call' | 'instantiate' | 'call_method' | 'dispose_instance',
|
|
561
|
-
params: cmd.params,
|
|
562
|
-
});
|
|
563
|
-
} catch (error) {
|
|
564
|
-
log.warn('Warmup command failed', { workerId: worker.id, error: String(error) });
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
await Promise.all(warmupPromises);
|
|
570
|
-
// Warmed up ${this.processPool.length} worker processes
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
private safeCacheKey(prefix: string, ...inputs: unknown[]): string | null {
|
|
574
|
-
try {
|
|
575
|
-
return globalCache.generateKey(prefix, ...inputs);
|
|
576
|
-
} catch {
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Heuristic to determine if function result should be cached
|
|
583
|
-
*/
|
|
584
|
-
private isPureFunctionCandidate(functionName: string, args: unknown[]): boolean {
|
|
585
|
-
// Simple heuristics - could be made more sophisticated
|
|
586
|
-
const pureFunctionPatterns = [
|
|
587
|
-
/^(get|fetch|read|load|find|search|query|select)_/i,
|
|
588
|
-
/^(compute|calculate|process|transform|convert)_/i,
|
|
589
|
-
/^(encode|decode|serialize|deserialize)_/i,
|
|
590
|
-
];
|
|
591
|
-
|
|
592
|
-
const impureFunctionPatterns = [
|
|
593
|
-
/^(set|save|write|update|insert|delete|create|modify)_/i,
|
|
594
|
-
/^(send|post|put|patch)_/i,
|
|
595
|
-
/random|uuid|timestamp|now|current/i,
|
|
596
|
-
];
|
|
597
|
-
|
|
598
|
-
// Don't cache if function name suggests mutation
|
|
599
|
-
if (impureFunctionPatterns.some(pattern => pattern.test(functionName))) {
|
|
600
|
-
return false;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Cache if function name suggests pure computation
|
|
604
|
-
if (pureFunctionPatterns.some(pattern => pattern.test(functionName))) {
|
|
605
|
-
return true;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Don't cache if args contain mutable objects (very basic check)
|
|
609
|
-
const hasComplexArgs = args.some(
|
|
610
|
-
arg => arg !== null && typeof arg === 'object' && !(arg instanceof Date)
|
|
611
|
-
);
|
|
612
|
-
|
|
613
|
-
return !hasComplexArgs && args.length <= 3; // Cache simple calls with few args
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/**
|
|
617
|
-
* Update performance statistics
|
|
618
|
-
*/
|
|
619
|
-
private updateStats(duration: number, _error: boolean = false): void {
|
|
620
|
-
this.stats.totalRequests++;
|
|
621
|
-
this.stats.totalTime += duration;
|
|
622
|
-
|
|
623
|
-
const currentMemory = process.memoryUsage().heapUsed;
|
|
624
|
-
if (currentMemory > this.stats.memoryPeak) {
|
|
625
|
-
this.stats.memoryPeak = currentMemory;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* Get performance statistics
|
|
631
|
-
*/
|
|
632
|
-
getStats(): OptimizedBridgeStatsSnapshot {
|
|
633
|
-
const avgTime =
|
|
634
|
-
this.stats.totalRequests > 0 ? this.stats.totalTime / this.stats.totalRequests : 0;
|
|
635
|
-
const hitRate =
|
|
636
|
-
this.stats.totalRequests > 0 ? this.stats.cacheHits / this.stats.totalRequests : 0;
|
|
637
|
-
|
|
638
|
-
return {
|
|
639
|
-
...this.stats,
|
|
640
|
-
averageTime: avgTime,
|
|
641
|
-
cacheHitRate: hitRate,
|
|
642
|
-
poolSize: this.processPool.length,
|
|
643
|
-
busyWorkers: this.processPool.filter(w => w.busy).length,
|
|
644
|
-
memoryUsage: process.memoryUsage(),
|
|
645
|
-
workerStats: this.processPool.map(w => ({
|
|
646
|
-
id: w.id,
|
|
647
|
-
requestCount: w.requestCount,
|
|
648
|
-
averageTime: w.stats.averageTime,
|
|
649
|
-
errorCount: w.stats.errorCount,
|
|
650
|
-
busy: w.busy,
|
|
651
|
-
pendingRequests: w.core.getPendingCount(),
|
|
652
|
-
})),
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Cleanup idle processes
|
|
658
|
-
*/
|
|
659
|
-
private async cleanup(): Promise<void> {
|
|
660
|
-
const now = Date.now();
|
|
661
|
-
const idleWorkers = this.processPool.filter(
|
|
662
|
-
w =>
|
|
663
|
-
!w.busy &&
|
|
664
|
-
now - w.lastUsed > this.options.maxIdleTime &&
|
|
665
|
-
this.processPool.length > this.options.minProcesses
|
|
666
|
-
);
|
|
667
|
-
|
|
668
|
-
for (const worker of idleWorkers) {
|
|
669
|
-
await this.terminateWorker(worker);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Restart workers that have handled too many requests
|
|
673
|
-
const overusedWorkers = this.processPool.filter(
|
|
674
|
-
w => !w.busy && w.requestCount >= this.options.maxRequestsPerProcess
|
|
675
|
-
);
|
|
676
|
-
|
|
677
|
-
for (const worker of overusedWorkers) {
|
|
678
|
-
await this.terminateWorker(worker);
|
|
679
|
-
if (this.processPool.length < this.options.minProcesses) {
|
|
680
|
-
await this.spawnProcess();
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Gracefully terminate a worker
|
|
687
|
-
*/
|
|
688
|
-
private async terminateWorker(
|
|
689
|
-
worker: WorkerProcess,
|
|
690
|
-
options: { force?: boolean } = {}
|
|
691
|
-
): Promise<void> {
|
|
692
|
-
if (worker.busy && !options.force) {
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const index = this.processPool.indexOf(worker);
|
|
697
|
-
if (index >= 0) {
|
|
698
|
-
this.processPool.splice(index, 1);
|
|
699
|
-
this.stats.processDeaths++;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
worker.core.handleProcessExit();
|
|
703
|
-
worker.core.clear();
|
|
704
|
-
|
|
705
|
-
// Graceful termination
|
|
706
|
-
try {
|
|
707
|
-
if (worker.process.exitCode === null) {
|
|
708
|
-
worker.process.kill('SIGTERM');
|
|
709
|
-
|
|
710
|
-
// Force kill if not terminated in 5 seconds
|
|
711
|
-
setTimeout(() => {
|
|
712
|
-
if (worker.process.exitCode === null) {
|
|
713
|
-
worker.process.kill('SIGKILL');
|
|
714
|
-
}
|
|
715
|
-
}, 5000);
|
|
716
|
-
}
|
|
717
|
-
} catch (error) {
|
|
718
|
-
log.warn('Error terminating worker', { workerId: worker.id, error: String(error) });
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Terminated worker ${worker.id}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Start cleanup scheduler
|
|
726
|
-
*/
|
|
727
|
-
private startCleanupScheduler(): void {
|
|
728
|
-
this.cleanupTimer = setInterval(async () => {
|
|
729
|
-
try {
|
|
730
|
-
await this.cleanup();
|
|
731
|
-
} catch (error) {
|
|
732
|
-
log.error('Cleanup error', { error: String(error) });
|
|
733
|
-
}
|
|
734
|
-
}, 60000); // Cleanup every minute
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* Dispose all resources
|
|
739
|
-
*/
|
|
740
|
-
async dispose(): Promise<void> {
|
|
741
|
-
if (this.disposed) {
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
this.disposed = true;
|
|
746
|
-
|
|
747
|
-
if (this.cleanupTimer) {
|
|
748
|
-
clearInterval(this.cleanupTimer);
|
|
749
|
-
this.cleanupTimer = undefined;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Terminate all workers
|
|
753
|
-
const terminationPromises = this.processPool.map(worker =>
|
|
754
|
-
this.terminateWorker(worker, { force: true })
|
|
755
|
-
);
|
|
756
|
-
await Promise.all(terminationPromises);
|
|
757
|
-
|
|
758
|
-
this.processPool.length = 0;
|
|
759
|
-
this.emitter.removeAllListeners();
|
|
760
|
-
|
|
761
|
-
// Disposed optimized Node.js bridge
|
|
762
|
-
}
|
|
763
|
-
}
|
|
21
|
+
export { NodeBridge as OptimizedNodeBridge, type NodeBridgeOptions as ProcessPoolOptions } from './node.js';
|