@cloudflare/sandbox 0.3.1 → 0.3.3
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/CHANGELOG.md +12 -0
- package/Dockerfile +22 -24
- package/README.md +2 -2
- package/container_src/bun.lock +31 -77
- package/container_src/control-process.ts +1 -1
- package/container_src/index.ts +34 -43
- package/container_src/interpreter-service.ts +276 -0
- package/container_src/isolation.ts +1 -1
- package/container_src/mime-processor.ts +1 -1
- package/container_src/package.json +4 -4
- package/container_src/runtime/executors/javascript/node_executor.ts +123 -0
- package/container_src/runtime/executors/python/ipython_executor.py +338 -0
- package/container_src/runtime/executors/typescript/ts_executor.ts +138 -0
- package/container_src/runtime/process-pool.ts +464 -0
- package/container_src/startup.sh +6 -79
- package/dist/{chunk-LALY4SFU.js → chunk-FXYPFGOZ.js} +10 -10
- package/dist/chunk-FXYPFGOZ.js.map +1 -0
- package/dist/{chunk-LFLJGISB.js → chunk-H4PW2LGW.js} +6 -6
- package/dist/chunk-H4PW2LGW.js.map +1 -0
- package/dist/{chunk-FKBV7CZS.js → chunk-JTKON2SH.js} +9 -9
- package/dist/chunk-JTKON2SH.js.map +1 -0
- package/dist/{chunk-EGC5IYXA.js → chunk-W7TVRPBG.js} +2 -2
- package/dist/chunk-W7TVRPBG.js.map +1 -0
- package/dist/{chunk-BEQUGUY4.js → chunk-Z6OZPC6U.js} +9 -6
- package/dist/chunk-Z6OZPC6U.js.map +1 -0
- package/dist/{client-Dny_ro_v.d.ts → client-COGWU6bz.d.ts} +3 -3
- package/dist/client.d.ts +1 -1
- package/dist/errors.d.ts +9 -9
- package/dist/errors.js +5 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -11
- package/dist/interpreter-client.d.ts +4 -0
- package/dist/interpreter-client.js +9 -0
- package/dist/interpreter-types.d.ts +5 -5
- package/dist/interpreter-types.js +1 -1
- package/dist/interpreter.d.ts +2 -2
- package/dist/interpreter.js +2 -2
- package/dist/request-handler.d.ts +1 -1
- package/dist/request-handler.js +5 -5
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +5 -5
- package/package.json +2 -2
- package/src/errors.ts +15 -14
- package/src/index.ts +16 -5
- package/src/{jupyter-client.ts → interpreter-client.ts} +6 -3
- package/src/interpreter-types.ts +102 -95
- package/src/interpreter.ts +8 -8
- package/src/sandbox.ts +3 -3
- package/container_src/jupyter-server.ts +0 -579
- package/container_src/jupyter-service.ts +0 -461
- package/container_src/jupyter_config.py +0 -48
- package/dist/chunk-BEQUGUY4.js.map +0 -1
- package/dist/chunk-EGC5IYXA.js.map +0 -1
- package/dist/chunk-FKBV7CZS.js.map +0 -1
- package/dist/chunk-LALY4SFU.js.map +0 -1
- package/dist/chunk-LFLJGISB.js.map +0 -1
- package/dist/jupyter-client.d.ts +0 -4
- package/dist/jupyter-client.js +0 -9
- /package/dist/{jupyter-client.js.map → interpreter-client.js.map} +0 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
export type InterpreterLanguage = "python" | "javascript" | "typescript";
|
|
5
|
+
|
|
6
|
+
export interface InterpreterProcess {
|
|
7
|
+
id: string;
|
|
8
|
+
language: InterpreterLanguage;
|
|
9
|
+
process: ChildProcess;
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
lastUsed: Date;
|
|
12
|
+
isAvailable: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ExecutionResult {
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
success: boolean;
|
|
19
|
+
executionId: string;
|
|
20
|
+
outputs?: RichOutput[];
|
|
21
|
+
error?: {
|
|
22
|
+
type: string;
|
|
23
|
+
message: string;
|
|
24
|
+
traceback?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RichOutput {
|
|
29
|
+
type: "text" | "image" | "jpeg" | "svg" | "html" | "json" | "latex" | "markdown" | "javascript" | "error";
|
|
30
|
+
data: string;
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PoolConfig {
|
|
35
|
+
maxProcesses: number;
|
|
36
|
+
idleTimeout: number; // milliseconds
|
|
37
|
+
minSize: number;
|
|
38
|
+
preWarmScript?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ExecutorPoolConfig extends PoolConfig {
|
|
42
|
+
executor: InterpreterLanguage;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_EXECUTOR_CONFIGS: Record<InterpreterLanguage, ExecutorPoolConfig> = {
|
|
46
|
+
python: {
|
|
47
|
+
executor: "python",
|
|
48
|
+
minSize: 3,
|
|
49
|
+
maxProcesses: 15,
|
|
50
|
+
idleTimeout: 5 * 60 * 1000, // 5 minutes
|
|
51
|
+
preWarmScript: `
|
|
52
|
+
import matplotlib.pyplot as plt
|
|
53
|
+
import pandas as pd
|
|
54
|
+
import numpy as np
|
|
55
|
+
import json
|
|
56
|
+
print(json.dumps({"status": "pre-warmed"}))
|
|
57
|
+
`
|
|
58
|
+
},
|
|
59
|
+
javascript: {
|
|
60
|
+
executor: "javascript",
|
|
61
|
+
minSize: 3,
|
|
62
|
+
maxProcesses: 10,
|
|
63
|
+
idleTimeout: 5 * 60 * 1000,
|
|
64
|
+
preWarmScript: `
|
|
65
|
+
const fs = require('fs');
|
|
66
|
+
const path = require('path');
|
|
67
|
+
const util = require('util');
|
|
68
|
+
const crypto = require('crypto');
|
|
69
|
+
for(let i = 0; i < 1000; i++) {
|
|
70
|
+
JSON.stringify({x: i, data: Math.random()});
|
|
71
|
+
}
|
|
72
|
+
console.log(JSON.stringify({"status": "pre-warmed"}));
|
|
73
|
+
`
|
|
74
|
+
},
|
|
75
|
+
typescript: {
|
|
76
|
+
executor: "typescript",
|
|
77
|
+
minSize: 3,
|
|
78
|
+
maxProcesses: 10,
|
|
79
|
+
idleTimeout: 5 * 60 * 1000,
|
|
80
|
+
preWarmScript: `
|
|
81
|
+
const { transformSync } = require('esbuild');
|
|
82
|
+
const warmupCode = 'interface Test { x: number; } const test: Test = { x: 42 }; test.x';
|
|
83
|
+
transformSync(warmupCode, { loader: 'ts', target: 'es2020', format: 'cjs' });
|
|
84
|
+
console.log(JSON.stringify({"status": "pre-warmed"}));
|
|
85
|
+
`
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export class ProcessPoolManager {
|
|
90
|
+
private pools: Map<InterpreterLanguage, InterpreterProcess[]> = new Map();
|
|
91
|
+
private poolConfigs: Map<InterpreterLanguage, ExecutorPoolConfig> = new Map();
|
|
92
|
+
private cleanupInterval?: NodeJS.Timeout;
|
|
93
|
+
|
|
94
|
+
constructor(customConfigs: Partial<Record<InterpreterLanguage, Partial<ExecutorPoolConfig>>> = {}) {
|
|
95
|
+
const executorEntries = Object.entries(DEFAULT_EXECUTOR_CONFIGS) as [InterpreterLanguage, ExecutorPoolConfig][];
|
|
96
|
+
|
|
97
|
+
for (const [executor, defaultConfig] of executorEntries) {
|
|
98
|
+
const userConfig = customConfigs[executor] || {};
|
|
99
|
+
const envMinSize = process.env[`${executor.toUpperCase()}_POOL_MIN_SIZE`];
|
|
100
|
+
const envMaxSize = process.env[`${executor.toUpperCase()}_POOL_MAX_SIZE`];
|
|
101
|
+
|
|
102
|
+
const config: ExecutorPoolConfig = {
|
|
103
|
+
...defaultConfig,
|
|
104
|
+
...userConfig,
|
|
105
|
+
// Environment variables override user config override defaults
|
|
106
|
+
minSize: envMinSize ? parseInt(envMinSize) : (userConfig.minSize || defaultConfig.minSize),
|
|
107
|
+
maxProcesses: envMaxSize ? parseInt(envMaxSize) : (userConfig.maxProcesses || defaultConfig.maxProcesses)
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
this.poolConfigs.set(executor, config);
|
|
111
|
+
this.pools.set(executor, []);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pythonConfig = this.poolConfigs.get("python");
|
|
115
|
+
if (pythonConfig) {
|
|
116
|
+
this.cleanupInterval = setInterval(() => {
|
|
117
|
+
this.cleanupIdleProcesses();
|
|
118
|
+
}, pythonConfig.idleTimeout / 2);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Start pre-warming in background - don't block constructor
|
|
122
|
+
this.startPreWarming().catch((error) => {
|
|
123
|
+
console.error('[ProcessPool] Pre-warming failed:', error);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async execute(
|
|
128
|
+
language: InterpreterLanguage,
|
|
129
|
+
code: string,
|
|
130
|
+
sessionId?: string,
|
|
131
|
+
timeout = 30000
|
|
132
|
+
): Promise<ExecutionResult> {
|
|
133
|
+
const totalStartTime = Date.now();
|
|
134
|
+
const process = await this.getProcess(language, sessionId);
|
|
135
|
+
const processAcquireTime = Date.now() - totalStartTime;
|
|
136
|
+
|
|
137
|
+
const executionId = randomUUID();
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const execStartTime = Date.now();
|
|
141
|
+
const result = await this.executeCode(process, code, executionId, timeout);
|
|
142
|
+
const execTime = Date.now() - execStartTime;
|
|
143
|
+
const totalTime = Date.now() - totalStartTime;
|
|
144
|
+
|
|
145
|
+
console.log(`[ProcessPool] Execution complete - Process acquire: ${processAcquireTime}ms, Code exec: ${execTime}ms, Total: ${totalTime}ms`);
|
|
146
|
+
return result;
|
|
147
|
+
} finally {
|
|
148
|
+
this.releaseProcess(process, sessionId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async getProcess(
|
|
153
|
+
language: InterpreterLanguage,
|
|
154
|
+
sessionId?: string
|
|
155
|
+
): Promise<InterpreterProcess> {
|
|
156
|
+
const pool = this.pools.get(language)!;
|
|
157
|
+
|
|
158
|
+
if (sessionId) {
|
|
159
|
+
const existingProcess = pool.find(
|
|
160
|
+
(p) => p.sessionId === sessionId && p.isAvailable
|
|
161
|
+
);
|
|
162
|
+
if (existingProcess) {
|
|
163
|
+
existingProcess.isAvailable = false;
|
|
164
|
+
existingProcess.lastUsed = new Date();
|
|
165
|
+
return existingProcess;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const availableProcess = pool.find((p) => p.isAvailable && !p.sessionId);
|
|
170
|
+
if (availableProcess) {
|
|
171
|
+
availableProcess.isAvailable = false;
|
|
172
|
+
availableProcess.sessionId = sessionId;
|
|
173
|
+
availableProcess.lastUsed = new Date();
|
|
174
|
+
return availableProcess;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const config = this.poolConfigs.get(language)!;
|
|
178
|
+
if (pool.length < config.maxProcesses) {
|
|
179
|
+
const newProcess = await this.createProcess(language, sessionId);
|
|
180
|
+
pool.push(newProcess);
|
|
181
|
+
return newProcess;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
const checkForAvailable = () => {
|
|
186
|
+
const available = pool.find((p) => p.isAvailable);
|
|
187
|
+
if (available) {
|
|
188
|
+
available.isAvailable = false;
|
|
189
|
+
available.sessionId = sessionId;
|
|
190
|
+
available.lastUsed = new Date();
|
|
191
|
+
resolve(available);
|
|
192
|
+
} else {
|
|
193
|
+
setTimeout(checkForAvailable, 100);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
checkForAvailable();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async createProcess(
|
|
201
|
+
language: InterpreterLanguage,
|
|
202
|
+
sessionId?: string
|
|
203
|
+
): Promise<InterpreterProcess> {
|
|
204
|
+
const startTime = Date.now();
|
|
205
|
+
const id = randomUUID();
|
|
206
|
+
let command: string;
|
|
207
|
+
let args: string[];
|
|
208
|
+
|
|
209
|
+
switch (language) {
|
|
210
|
+
case "python":
|
|
211
|
+
command = "python3";
|
|
212
|
+
args = ["-u", "/container-server/runtime/executors/python/ipython_executor.py"];
|
|
213
|
+
break;
|
|
214
|
+
case "javascript":
|
|
215
|
+
command = "node";
|
|
216
|
+
args = ["/container-server/runtime/executors/javascript/node_executor.js"];
|
|
217
|
+
break;
|
|
218
|
+
case "typescript":
|
|
219
|
+
command = "node";
|
|
220
|
+
args = ["/container-server/runtime/executors/typescript/ts_executor.js"];
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(`[ProcessPool] Spawning ${language} process: ${command} ${args.join(' ')}`);
|
|
225
|
+
|
|
226
|
+
const childProcess = spawn(command, args, {
|
|
227
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
228
|
+
env: {
|
|
229
|
+
...process.env,
|
|
230
|
+
PYTHONUNBUFFERED: "1",
|
|
231
|
+
NODE_NO_WARNINGS: "1",
|
|
232
|
+
},
|
|
233
|
+
cwd: "/workspace",
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const interpreterProcess: InterpreterProcess = {
|
|
237
|
+
id,
|
|
238
|
+
language,
|
|
239
|
+
process: childProcess,
|
|
240
|
+
sessionId,
|
|
241
|
+
lastUsed: new Date(),
|
|
242
|
+
isAvailable: false,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
let readyBuffer = "";
|
|
247
|
+
let errorBuffer = "";
|
|
248
|
+
|
|
249
|
+
const timeout = setTimeout(() => {
|
|
250
|
+
childProcess.kill();
|
|
251
|
+
console.error(`[ProcessPool] ${language} executor timeout. stdout: "${readyBuffer}", stderr: "${errorBuffer}"`);
|
|
252
|
+
reject(new Error(`${language} executor failed to start`));
|
|
253
|
+
}, 5000);
|
|
254
|
+
|
|
255
|
+
const readyHandler = (data: Buffer) => {
|
|
256
|
+
readyBuffer += data.toString();
|
|
257
|
+
console.log(`[ProcessPool] ${language} stdout:`, data.toString());
|
|
258
|
+
|
|
259
|
+
if (readyBuffer.includes('"ready"')) {
|
|
260
|
+
clearTimeout(timeout);
|
|
261
|
+
childProcess.stdout?.removeListener("data", readyHandler);
|
|
262
|
+
childProcess.stderr?.removeListener("data", errorHandler);
|
|
263
|
+
const readyTime = Date.now() - startTime;
|
|
264
|
+
console.log(`[ProcessPool] ${language} process ${id} ready in ${readyTime}ms`);
|
|
265
|
+
resolve(interpreterProcess);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const errorHandler = (data: Buffer) => {
|
|
270
|
+
errorBuffer += data.toString();
|
|
271
|
+
console.error(`[ProcessPool] ${language} stderr:`, data.toString());
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
childProcess.stdout?.on("data", readyHandler);
|
|
275
|
+
childProcess.stderr?.on("data", errorHandler);
|
|
276
|
+
|
|
277
|
+
childProcess.once("error", (err) => {
|
|
278
|
+
clearTimeout(timeout);
|
|
279
|
+
console.error(`[ProcessPool] ${language} spawn error:`, err);
|
|
280
|
+
reject(err);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
childProcess.once("exit", (code) => {
|
|
284
|
+
if (code !== 0) {
|
|
285
|
+
clearTimeout(timeout);
|
|
286
|
+
console.error(`[ProcessPool] ${language} exited with code ${code}`);
|
|
287
|
+
reject(new Error(`${language} executor exited with code ${code}`));
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async executeCode(
|
|
294
|
+
process: InterpreterProcess,
|
|
295
|
+
code: string,
|
|
296
|
+
executionId: string,
|
|
297
|
+
timeout: number
|
|
298
|
+
): Promise<ExecutionResult> {
|
|
299
|
+
const request = JSON.stringify({ code, executionId });
|
|
300
|
+
|
|
301
|
+
return new Promise((resolve, reject) => {
|
|
302
|
+
const timer = setTimeout(() => {
|
|
303
|
+
reject(new Error("Execution timeout"));
|
|
304
|
+
}, timeout);
|
|
305
|
+
|
|
306
|
+
let responseBuffer = "";
|
|
307
|
+
|
|
308
|
+
const responseHandler = (data: Buffer) => {
|
|
309
|
+
responseBuffer += data.toString();
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const response = JSON.parse(responseBuffer);
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
process.process.stdout?.removeListener("data", responseHandler);
|
|
315
|
+
|
|
316
|
+
resolve({
|
|
317
|
+
stdout: response.stdout || "",
|
|
318
|
+
stderr: response.stderr || "",
|
|
319
|
+
success: response.success !== false,
|
|
320
|
+
executionId,
|
|
321
|
+
outputs: response.outputs || [],
|
|
322
|
+
error: response.error || null,
|
|
323
|
+
});
|
|
324
|
+
} catch (e) {
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
process.process.stdout?.on("data", responseHandler);
|
|
329
|
+
process.process.stdin?.write(`${request}\n`);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private releaseProcess(
|
|
334
|
+
process: InterpreterProcess,
|
|
335
|
+
sessionId?: string
|
|
336
|
+
): void {
|
|
337
|
+
if (!sessionId) {
|
|
338
|
+
process.sessionId = undefined;
|
|
339
|
+
process.isAvailable = true;
|
|
340
|
+
} else {
|
|
341
|
+
process.isAvailable = true;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private async startPreWarming(): Promise<void> {
|
|
346
|
+
console.log('[ProcessPool] Starting unified pre-warming for all executors...');
|
|
347
|
+
const startTime = Date.now();
|
|
348
|
+
|
|
349
|
+
const warmupPromises = Array.from(this.poolConfigs.entries()).map(
|
|
350
|
+
async ([executor, config]) => {
|
|
351
|
+
if (config.minSize > 0) {
|
|
352
|
+
await this.preWarmExecutor(executor, config);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
await Promise.all(warmupPromises);
|
|
359
|
+
const totalTime = Date.now() - startTime;
|
|
360
|
+
console.log(`[ProcessPool] Pre-warming complete for all executors in ${totalTime}ms`);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error('[ProcessPool] Pre-warming failed:', error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private async preWarmExecutor(executor: InterpreterLanguage, config: ExecutorPoolConfig): Promise<void> {
|
|
367
|
+
const startTime = Date.now();
|
|
368
|
+
console.log(`[ProcessPool] Pre-warming ${config.minSize} ${executor} processes...`);
|
|
369
|
+
|
|
370
|
+
const pool = this.pools.get(executor);
|
|
371
|
+
if (!pool) {
|
|
372
|
+
console.error(`[ProcessPool] No pool found for executor: ${executor}`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (let i = 0; i < config.minSize; i++) {
|
|
377
|
+
try {
|
|
378
|
+
const sessionId = `pre-warm-${executor}-${i}-${Date.now()}`;
|
|
379
|
+
const process = await this.createProcess(executor, sessionId);
|
|
380
|
+
|
|
381
|
+
if (config.preWarmScript) {
|
|
382
|
+
await this.executePreWarmScript(process, config.preWarmScript, executor);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
process.isAvailable = true;
|
|
386
|
+
process.sessionId = undefined;
|
|
387
|
+
pool.push(process);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error(`[ProcessPool] Failed to pre-warm ${executor} process ${i}:`, error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const warmupTime = Date.now() - startTime;
|
|
394
|
+
const actualCount = pool.filter(p => p.isAvailable).length;
|
|
395
|
+
console.log(`[ProcessPool] Pre-warmed ${actualCount}/${config.minSize} ${executor} processes in ${warmupTime}ms`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private async executePreWarmScript(
|
|
399
|
+
process: InterpreterProcess,
|
|
400
|
+
script: string,
|
|
401
|
+
executor: InterpreterLanguage
|
|
402
|
+
): Promise<void> {
|
|
403
|
+
try {
|
|
404
|
+
const executionId = `pre-warm-${Date.now()}`;
|
|
405
|
+
const result = await this.executeCode(process, script, executionId, 10000);
|
|
406
|
+
|
|
407
|
+
if (result.success) {
|
|
408
|
+
console.log(`[ProcessPool] ${executor} pre-warm script executed successfully`);
|
|
409
|
+
} else {
|
|
410
|
+
console.warn(`[ProcessPool] ${executor} pre-warm script failed:`, result.stderr);
|
|
411
|
+
}
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.warn(`[ProcessPool] ${executor} pre-warm script error:`, error);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private cleanupIdleProcesses(): void {
|
|
418
|
+
const now = new Date();
|
|
419
|
+
|
|
420
|
+
const executors = Array.from(this.pools.keys());
|
|
421
|
+
for (const executor of executors) {
|
|
422
|
+
const pool = this.pools.get(executor);
|
|
423
|
+
const config = this.poolConfigs.get(executor);
|
|
424
|
+
|
|
425
|
+
if (!pool || !config) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (let i = pool.length - 1; i >= 0; i--) {
|
|
430
|
+
const process = pool[i];
|
|
431
|
+
const idleTime = now.getTime() - process.lastUsed.getTime();
|
|
432
|
+
|
|
433
|
+
// Only clean up excess processes beyond minimum pool size
|
|
434
|
+
if (process.isAvailable &&
|
|
435
|
+
idleTime > config.idleTimeout &&
|
|
436
|
+
pool.filter(p => p.isAvailable).length > config.minSize) {
|
|
437
|
+
process.process.kill();
|
|
438
|
+
pool.splice(i, 1);
|
|
439
|
+
console.log(`[ProcessPool] Cleaned up idle ${executor} process (${pool.length} remaining)`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async shutdown(): Promise<void> {
|
|
446
|
+
if (this.cleanupInterval) {
|
|
447
|
+
clearInterval(this.cleanupInterval);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const executors = Array.from(this.pools.keys());
|
|
451
|
+
for (const executor of executors) {
|
|
452
|
+
const pool = this.pools.get(executor);
|
|
453
|
+
if (pool) {
|
|
454
|
+
for (const process of pool) {
|
|
455
|
+
process.process.kill();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
this.pools.clear();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export const processPool = new ProcessPoolManager();
|
package/container_src/startup.sh
CHANGED
|
@@ -1,84 +1,11 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# Check if API is responsive and kernelspecs are available
|
|
6
|
-
curl -s http://localhost:8888/api/kernelspecs > /dev/null 2>&1
|
|
7
|
-
}
|
|
3
|
+
echo "[Startup] Starting interpreter server..."
|
|
4
|
+
echo "[Startup] Process pool system initialized"
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# Create a marker file that the Bun server can check
|
|
12
|
-
touch /tmp/jupyter-ready
|
|
13
|
-
echo "[Startup] Jupyter is ready, notified Bun server"
|
|
14
|
-
}
|
|
6
|
+
echo "[Startup] Environment configured:"
|
|
7
|
+
echo " - Working directory: $(pwd)"
|
|
15
8
|
|
|
16
|
-
# Start
|
|
17
|
-
echo "[Startup] Starting Jupyter server..."
|
|
18
|
-
jupyter server \
|
|
19
|
-
--config=/container-server/jupyter_config.py \
|
|
20
|
-
> /tmp/jupyter.log 2>&1 &
|
|
21
|
-
|
|
22
|
-
JUPYTER_PID=$!
|
|
23
|
-
|
|
24
|
-
# Start Bun server immediately (parallel startup)
|
|
9
|
+
# Start Bun server - the only process we need now
|
|
25
10
|
echo "[Startup] Starting Bun server..."
|
|
26
|
-
bun index.ts
|
|
27
|
-
BUN_PID=$!
|
|
28
|
-
|
|
29
|
-
# Monitor Jupyter readiness in background
|
|
30
|
-
(
|
|
31
|
-
echo "[Startup] Monitoring Jupyter readiness in background..."
|
|
32
|
-
MAX_ATTEMPTS=60
|
|
33
|
-
ATTEMPT=0
|
|
34
|
-
|
|
35
|
-
# Track start time for reporting
|
|
36
|
-
START_TIME=$(date +%s.%N)
|
|
37
|
-
|
|
38
|
-
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
|
39
|
-
if check_jupyter_ready; then
|
|
40
|
-
notify_jupyter_ready
|
|
41
|
-
END_TIME=$(date +%s.%N)
|
|
42
|
-
ELAPSED=$(awk "BEGIN {printf \"%.2f\", $END_TIME - $START_TIME}")
|
|
43
|
-
echo "[Startup] Jupyter server is ready after $ELAPSED seconds ($ATTEMPT attempts)"
|
|
44
|
-
break
|
|
45
|
-
fi
|
|
46
|
-
|
|
47
|
-
# Check if Jupyter process is still running
|
|
48
|
-
if ! kill -0 $JUPYTER_PID 2>/dev/null; then
|
|
49
|
-
echo "[Startup] WARNING: Jupyter process died. Check /tmp/jupyter.log for details"
|
|
50
|
-
cat /tmp/jupyter.log
|
|
51
|
-
# Don't exit - let Bun server continue running in degraded mode
|
|
52
|
-
break
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
ATTEMPT=$((ATTEMPT + 1))
|
|
56
|
-
|
|
57
|
-
# Start with faster checks
|
|
58
|
-
if [ $ATTEMPT -eq 1 ]; then
|
|
59
|
-
DELAY=0.5 # Start at 0.5s
|
|
60
|
-
else
|
|
61
|
-
# Exponential backoff with 1.3x multiplier (less aggressive than 1.5x)
|
|
62
|
-
DELAY=$(awk "BEGIN {printf \"%.2f\", $DELAY * 1.3}")
|
|
63
|
-
# Cap at 2s max (instead of 5s)
|
|
64
|
-
if [ $(awk "BEGIN {print ($DELAY > 2)}") -eq 1 ]; then
|
|
65
|
-
DELAY=2
|
|
66
|
-
fi
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
# Log with current delay for transparency
|
|
70
|
-
echo "[Startup] Jupyter not ready yet (attempt $ATTEMPT/$MAX_ATTEMPTS, next check in ${DELAY}s)"
|
|
71
|
-
|
|
72
|
-
sleep $DELAY
|
|
73
|
-
done
|
|
74
|
-
|
|
75
|
-
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
|
|
76
|
-
echo "[Startup] WARNING: Jupyter failed to become ready within attempts"
|
|
77
|
-
echo "[Startup] Jupyter logs:"
|
|
78
|
-
cat /tmp/jupyter.log
|
|
79
|
-
# Don't exit - let Bun server continue in degraded mode
|
|
80
|
-
fi
|
|
81
|
-
) &
|
|
82
|
-
|
|
83
|
-
# Wait for Bun server (main process)
|
|
84
|
-
wait $BUN_PID
|
|
11
|
+
exec bun index.ts
|
|
@@ -8,13 +8,13 @@ var SandboxError = class extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
};
|
|
11
|
-
var
|
|
12
|
-
code = "
|
|
11
|
+
var InterpreterNotReadyError = class extends SandboxError {
|
|
12
|
+
code = "INTERPRETER_NOT_READY";
|
|
13
13
|
retryAfter;
|
|
14
14
|
progress;
|
|
15
15
|
constructor(message, options) {
|
|
16
16
|
super(
|
|
17
|
-
message || "
|
|
17
|
+
message || "Interpreter is still initializing. Please retry in a few seconds."
|
|
18
18
|
);
|
|
19
19
|
this.retryAfter = options?.retryAfter || 5;
|
|
20
20
|
this.progress = options?.progress;
|
|
@@ -62,14 +62,14 @@ var ServiceUnavailableError = class extends SandboxError {
|
|
|
62
62
|
this.retryAfter = retryAfter;
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
|
-
function
|
|
66
|
-
return error instanceof
|
|
65
|
+
function isInterpreterNotReadyError(error) {
|
|
66
|
+
return error instanceof InterpreterNotReadyError;
|
|
67
67
|
}
|
|
68
68
|
function isSandboxError(error) {
|
|
69
69
|
return error instanceof SandboxError;
|
|
70
70
|
}
|
|
71
71
|
function isRetryableError(error) {
|
|
72
|
-
if (error instanceof
|
|
72
|
+
if (error instanceof InterpreterNotReadyError || error instanceof ContainerNotReadyError || error instanceof ServiceUnavailableError) {
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
75
|
if (error instanceof SandboxNetworkError) {
|
|
@@ -96,7 +96,7 @@ async function parseErrorResponse(response) {
|
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
if (data.status === "initializing") {
|
|
99
|
-
return new
|
|
99
|
+
return new InterpreterNotReadyError(data.error, {
|
|
100
100
|
retryAfter: parseInt(response.headers.get("Retry-After") || "5"),
|
|
101
101
|
progress: data.progress
|
|
102
102
|
});
|
|
@@ -115,15 +115,15 @@ async function parseErrorResponse(response) {
|
|
|
115
115
|
|
|
116
116
|
export {
|
|
117
117
|
SandboxError,
|
|
118
|
-
|
|
118
|
+
InterpreterNotReadyError,
|
|
119
119
|
ContextNotFoundError,
|
|
120
120
|
CodeExecutionError,
|
|
121
121
|
ContainerNotReadyError,
|
|
122
122
|
SandboxNetworkError,
|
|
123
123
|
ServiceUnavailableError,
|
|
124
|
-
|
|
124
|
+
isInterpreterNotReadyError,
|
|
125
125
|
isSandboxError,
|
|
126
126
|
isRetryableError,
|
|
127
127
|
parseErrorResponse
|
|
128
128
|
};
|
|
129
|
-
//# sourceMappingURL=chunk-
|
|
129
|
+
//# sourceMappingURL=chunk-FXYPFGOZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Standard error response from the sandbox API\n */\nexport interface SandboxErrorResponse {\n error?: string;\n status?: string;\n progress?: number;\n}\n\n/**\n * Base error class for all Sandbox-related errors\n */\nexport class SandboxError extends Error {\n constructor(message: string) {\n super(message);\n this.name = this.constructor.name;\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/**\n * Error thrown when interpreter functionality is requested but the service is still initializing.\n *\n * Note: With the current implementation, requests wait for interpreter to be ready.\n * This error is only thrown when:\n * 1. The request times out waiting for interpreter (default: 30 seconds)\n * 2. interpreter initialization actually fails\n *\n * Most requests will succeed after a delay, not throw this error.\n */\nexport class InterpreterNotReadyError extends SandboxError {\n public readonly code = \"INTERPRETER_NOT_READY\";\n public readonly retryAfter: number;\n public readonly progress?: number;\n\n constructor(\n message?: string,\n options?: { retryAfter?: number; progress?: number }\n ) {\n super(\n message ||\n \"Interpreter is still initializing. Please retry in a few seconds.\"\n );\n this.retryAfter = options?.retryAfter || 5;\n this.progress = options?.progress;\n }\n}\n\n/**\n * Error thrown when a context is not found\n */\nexport class ContextNotFoundError extends SandboxError {\n public readonly code = \"CONTEXT_NOT_FOUND\";\n public readonly contextId: string;\n\n constructor(contextId: string) {\n super(`Context ${contextId} not found`);\n this.contextId = contextId;\n }\n}\n\n/**\n * Error thrown when code execution fails\n */\nexport class CodeExecutionError extends SandboxError {\n public readonly code = \"CODE_EXECUTION_ERROR\";\n public readonly executionError?: {\n ename?: string;\n evalue?: string;\n traceback?: string[];\n };\n\n constructor(message: string, executionError?: any) {\n super(message);\n this.executionError = executionError;\n }\n}\n\n/**\n * Error thrown when the sandbox container is not ready\n */\nexport class ContainerNotReadyError extends SandboxError {\n public readonly code = \"CONTAINER_NOT_READY\";\n\n constructor(message?: string) {\n super(\n message ||\n \"Container is not ready. Please wait for initialization to complete.\"\n );\n }\n}\n\n/**\n * Error thrown when a network request to the sandbox fails\n */\nexport class SandboxNetworkError extends SandboxError {\n public readonly code = \"NETWORK_ERROR\";\n public readonly statusCode?: number;\n public readonly statusText?: string;\n\n constructor(message: string, statusCode?: number, statusText?: string) {\n super(message);\n this.statusCode = statusCode;\n this.statusText = statusText;\n }\n}\n\n/**\n * Error thrown when service is temporarily unavailable (e.g., circuit breaker open)\n */\nexport class ServiceUnavailableError extends SandboxError {\n public readonly code = \"SERVICE_UNAVAILABLE\";\n public readonly retryAfter?: number;\n\n constructor(message?: string, retryAfter?: number) {\n // Simple, user-friendly message without implementation details\n super(message || \"Service temporarily unavailable\");\n this.retryAfter = retryAfter;\n }\n}\n\n/**\n * Type guard to check if an error is a InterpreterNotReadyError\n */\nexport function isInterpreterNotReadyError(\n error: unknown\n): error is InterpreterNotReadyError {\n return error instanceof InterpreterNotReadyError;\n}\n\n/**\n * Type guard to check if an error is any SandboxError\n */\nexport function isSandboxError(error: unknown): error is SandboxError {\n return error instanceof SandboxError;\n}\n\n/**\n * Helper to determine if an error is retryable\n */\nexport function isRetryableError(error: unknown): boolean {\n if (\n error instanceof InterpreterNotReadyError ||\n error instanceof ContainerNotReadyError ||\n error instanceof ServiceUnavailableError\n ) {\n return true;\n }\n\n if (error instanceof SandboxNetworkError) {\n // Retry on 502, 503, 504 (gateway/service unavailable errors)\n return error.statusCode\n ? [502, 503, 504].includes(error.statusCode)\n : false;\n }\n\n return false;\n}\n\n/**\n * Parse error response from the sandbox API and return appropriate error instance\n */\nexport async function parseErrorResponse(\n response: Response\n): Promise<SandboxError> {\n let data: SandboxErrorResponse;\n\n try {\n data = (await response.json()) as SandboxErrorResponse;\n } catch {\n // If JSON parsing fails, return a generic network error\n return new SandboxNetworkError(\n `Request failed with status ${response.status}`,\n response.status,\n response.statusText\n );\n }\n\n // Check for specific error types based on response\n if (response.status === 503) {\n // Circuit breaker error\n if (data.status === \"circuit_open\") {\n return new ServiceUnavailableError(\n \"Service temporarily unavailable\",\n parseInt(response.headers.get(\"Retry-After\") || \"30\")\n );\n }\n\n // Interpreter initialization error\n if (data.status === \"initializing\") {\n return new InterpreterNotReadyError(data.error, {\n retryAfter: parseInt(response.headers.get(\"Retry-After\") || \"5\"),\n progress: data.progress,\n });\n }\n }\n\n // Check for context not found\n if (\n response.status === 404 &&\n data.error?.includes(\"Context\") &&\n data.error?.includes(\"not found\")\n ) {\n const contextId =\n data.error.match(/Context (\\S+) not found/)?.[1] || \"unknown\";\n return new ContextNotFoundError(contextId);\n }\n\n // Default network error\n return new SandboxNetworkError(\n data.error || `Request failed with status ${response.status}`,\n response.status,\n response.statusText\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
validatePort
|
|
6
6
|
} from "./chunk-6UAWTJ5S.js";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
InterpreterClient
|
|
9
|
+
} from "./chunk-Z6OZPC6U.js";
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
} from "./chunk-
|
|
11
|
+
CodeInterpreter
|
|
12
|
+
} from "./chunk-JTKON2SH.js";
|
|
13
13
|
|
|
14
14
|
// src/sandbox.ts
|
|
15
15
|
import { Container, getContainer } from "@cloudflare/containers";
|
|
@@ -130,7 +130,7 @@ var Sandbox = class extends Container {
|
|
|
130
130
|
defaultSession = null;
|
|
131
131
|
constructor(ctx, env) {
|
|
132
132
|
super(ctx, env);
|
|
133
|
-
this.client = new
|
|
133
|
+
this.client = new InterpreterClient({
|
|
134
134
|
onCommandComplete: (success, exitCode, _stdout, _stderr, command) => {
|
|
135
135
|
console.log(
|
|
136
136
|
`[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
|
|
@@ -664,4 +664,4 @@ export {
|
|
|
664
664
|
proxyToSandbox,
|
|
665
665
|
isLocalhostPattern
|
|
666
666
|
};
|
|
667
|
-
//# sourceMappingURL=chunk-
|
|
667
|
+
//# sourceMappingURL=chunk-H4PW2LGW.js.map
|