@cloudflare/sandbox 0.2.1 → 0.2.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 +10 -6
- package/README.md +1 -1
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/index.ts +228 -103
- package/container_src/jupyter-server.ts +289 -46
- package/container_src/jupyter-service.ts +458 -0
- package/container_src/jupyter_config.py +48 -0
- package/container_src/startup.sh +74 -42
- package/dist/{chunk-IATLC32Y.js → chunk-4KELYYKS.js} +10 -10
- package/dist/chunk-4KELYYKS.js.map +1 -0
- package/dist/chunk-LALY4SFU.js +129 -0
- package/dist/chunk-LALY4SFU.js.map +1 -0
- package/dist/{chunk-SYMWNYWA.js → chunk-VTKZL632.js} +116 -64
- package/dist/chunk-VTKZL632.js.map +1 -0
- package/dist/{client-C7rKCYBD.d.ts → client-bzEV222a.d.ts} +10 -0
- package/dist/client.d.ts +1 -1
- package/dist/errors.d.ts +95 -0
- package/dist/errors.js +27 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +27 -3
- package/dist/interpreter.d.ts +1 -1
- package/dist/jupyter-client.d.ts +1 -1
- package/dist/jupyter-client.js +2 -1
- package/dist/request-handler.d.ts +1 -1
- package/dist/request-handler.js +4 -3
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +4 -3
- package/package.json +1 -1
- package/src/errors.ts +218 -0
- package/src/index.ts +33 -8
- package/src/jupyter-client.ts +225 -142
- package/src/sandbox.ts +1 -1
- package/dist/chunk-IATLC32Y.js.map +0 -1
- package/dist/chunk-SYMWNYWA.js.map +0 -1
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { CircuitBreaker } from "./circuit-breaker";
|
|
3
|
+
import { type CreateContextRequest, JupyterServer } from "./jupyter-server";
|
|
4
|
+
|
|
5
|
+
interface PoolStats {
|
|
6
|
+
available: number;
|
|
7
|
+
inUse: number;
|
|
8
|
+
total: number;
|
|
9
|
+
minSize: number;
|
|
10
|
+
maxSize: number;
|
|
11
|
+
warming: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CircuitBreakerState {
|
|
15
|
+
state: string;
|
|
16
|
+
failures: number;
|
|
17
|
+
lastFailure: number;
|
|
18
|
+
isOpen: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface JupyterHealthStatus {
|
|
22
|
+
ready: boolean;
|
|
23
|
+
initializing: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
checks?: {
|
|
26
|
+
httpApi: boolean;
|
|
27
|
+
kernelManager: boolean;
|
|
28
|
+
markerFile: boolean;
|
|
29
|
+
kernelSpawn?: boolean;
|
|
30
|
+
websocket?: boolean;
|
|
31
|
+
};
|
|
32
|
+
timestamp: number;
|
|
33
|
+
performance?: {
|
|
34
|
+
poolStats?: Record<string, PoolStats>;
|
|
35
|
+
activeContexts?: number;
|
|
36
|
+
uptime?: number;
|
|
37
|
+
circuitBreakers?: {
|
|
38
|
+
contextCreation: CircuitBreakerState;
|
|
39
|
+
codeExecution: CircuitBreakerState;
|
|
40
|
+
kernelCommunication: CircuitBreakerState;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Wrapper service that provides graceful degradation for Jupyter functionality
|
|
47
|
+
*/
|
|
48
|
+
export class JupyterService {
|
|
49
|
+
private jupyterServer: JupyterServer;
|
|
50
|
+
private initPromise: Promise<void> | null = null;
|
|
51
|
+
private initialized = false;
|
|
52
|
+
private initError: Error | null = null;
|
|
53
|
+
private startTime = Date.now();
|
|
54
|
+
private circuitBreakers: {
|
|
55
|
+
contextCreation: CircuitBreaker;
|
|
56
|
+
codeExecution: CircuitBreaker;
|
|
57
|
+
kernelCommunication: CircuitBreaker;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
constructor() {
|
|
61
|
+
this.jupyterServer = new JupyterServer();
|
|
62
|
+
|
|
63
|
+
// Initialize circuit breakers for different operations
|
|
64
|
+
this.circuitBreakers = {
|
|
65
|
+
contextCreation: new CircuitBreaker({
|
|
66
|
+
name: "context-creation",
|
|
67
|
+
threshold: 3,
|
|
68
|
+
timeout: 60000, // 1 minute
|
|
69
|
+
}),
|
|
70
|
+
codeExecution: new CircuitBreaker({
|
|
71
|
+
name: "code-execution",
|
|
72
|
+
threshold: 5,
|
|
73
|
+
timeout: 30000, // 30 seconds
|
|
74
|
+
}),
|
|
75
|
+
kernelCommunication: new CircuitBreaker({
|
|
76
|
+
name: "kernel-communication",
|
|
77
|
+
threshold: 10,
|
|
78
|
+
timeout: 20000, // 20 seconds
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Initialize Jupyter server with retry logic
|
|
85
|
+
*/
|
|
86
|
+
async initialize(): Promise<void> {
|
|
87
|
+
if (this.initialized) return;
|
|
88
|
+
|
|
89
|
+
if (!this.initPromise) {
|
|
90
|
+
this.initPromise = this.doInitialize()
|
|
91
|
+
.then(() => {
|
|
92
|
+
this.initialized = true;
|
|
93
|
+
console.log("[JupyterService] Initialization complete");
|
|
94
|
+
})
|
|
95
|
+
.catch((err) => {
|
|
96
|
+
this.initError = err;
|
|
97
|
+
// Don't null out initPromise on error - keep it so we can return the same error
|
|
98
|
+
console.error("[JupyterService] Initialization failed:", err);
|
|
99
|
+
throw err;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return this.initPromise;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async doInitialize(): Promise<void> {
|
|
107
|
+
// Wait for Jupyter marker file or timeout
|
|
108
|
+
const markerCheckPromise = this.waitForMarkerFile();
|
|
109
|
+
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
110
|
+
setTimeout(
|
|
111
|
+
() => reject(new Error("Jupyter initialization timeout")),
|
|
112
|
+
60000
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await Promise.race([markerCheckPromise, timeoutPromise]);
|
|
118
|
+
console.log("[JupyterService] Jupyter process detected via marker file");
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.log(
|
|
121
|
+
"[JupyterService] Marker file not found yet - proceeding with initialization"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Initialize Jupyter server
|
|
126
|
+
await this.jupyterServer.initialize();
|
|
127
|
+
|
|
128
|
+
// Pre-warm context pools in background after Jupyter is ready
|
|
129
|
+
this.warmContextPools();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Pre-warm context pools for better performance
|
|
134
|
+
*/
|
|
135
|
+
private async warmContextPools() {
|
|
136
|
+
// Delay pre-warming to avoid startup rush that triggers Bun WebSocket bug
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
console.log(
|
|
141
|
+
"[JupyterService] Pre-warming context pools for better performance"
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Warm one at a time with delay between them
|
|
145
|
+
await this.jupyterServer.enablePoolWarming("python", 1);
|
|
146
|
+
|
|
147
|
+
// Small delay between different language kernels
|
|
148
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
149
|
+
|
|
150
|
+
await this.jupyterServer.enablePoolWarming("javascript", 1);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error("[JupyterService] Error pre-warming context pools:", error);
|
|
153
|
+
console.error(
|
|
154
|
+
"[JupyterService] Pre-warming failed but service continues"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async waitForMarkerFile(): Promise<void> {
|
|
160
|
+
const markerPath = "/tmp/jupyter-ready";
|
|
161
|
+
let attempts = 0;
|
|
162
|
+
const maxAttempts = 120; // 2 minutes with 1s intervals
|
|
163
|
+
|
|
164
|
+
while (attempts < maxAttempts) {
|
|
165
|
+
if (existsSync(markerPath)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
attempts++;
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw new Error("Marker file not found within timeout");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get current health status
|
|
177
|
+
*/
|
|
178
|
+
async getHealthStatus(): Promise<JupyterHealthStatus> {
|
|
179
|
+
const status: JupyterHealthStatus = {
|
|
180
|
+
ready: this.initialized,
|
|
181
|
+
initializing: this.initPromise !== null && !this.initialized,
|
|
182
|
+
timestamp: Date.now(),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (this.initError) {
|
|
186
|
+
status.error = this.initError.message;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Detailed health checks
|
|
190
|
+
if (this.initialized || this.initPromise) {
|
|
191
|
+
status.checks = {
|
|
192
|
+
httpApi: await this.checkHttpApi(),
|
|
193
|
+
kernelManager: this.initialized,
|
|
194
|
+
markerFile: existsSync("/tmp/jupyter-ready"),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Advanced health checks only if fully initialized
|
|
198
|
+
if (this.initialized) {
|
|
199
|
+
const advancedChecks = await this.performAdvancedHealthChecks();
|
|
200
|
+
status.checks.kernelSpawn = advancedChecks.kernelSpawn;
|
|
201
|
+
status.checks.websocket = advancedChecks.websocket;
|
|
202
|
+
|
|
203
|
+
// Performance metrics
|
|
204
|
+
status.performance = {
|
|
205
|
+
poolStats: await this.jupyterServer.getPoolStats(),
|
|
206
|
+
activeContexts: (await this.jupyterServer.listContexts()).length,
|
|
207
|
+
uptime: Date.now() - this.startTime,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Add circuit breaker status
|
|
213
|
+
if (status.performance) {
|
|
214
|
+
status.performance.circuitBreakers = {
|
|
215
|
+
contextCreation: this.circuitBreakers.contextCreation.getState(),
|
|
216
|
+
codeExecution: this.circuitBreakers.codeExecution.getState(),
|
|
217
|
+
kernelCommunication:
|
|
218
|
+
this.circuitBreakers.kernelCommunication.getState(),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return status;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async checkHttpApi(): Promise<boolean> {
|
|
226
|
+
try {
|
|
227
|
+
const response = await fetch("http://localhost:8888/api", {
|
|
228
|
+
signal: AbortSignal.timeout(2000),
|
|
229
|
+
});
|
|
230
|
+
return response.ok;
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Perform advanced health checks including kernel spawn and WebSocket
|
|
238
|
+
*/
|
|
239
|
+
private async performAdvancedHealthChecks(): Promise<{
|
|
240
|
+
kernelSpawn: boolean;
|
|
241
|
+
websocket: boolean;
|
|
242
|
+
}> {
|
|
243
|
+
const checks = {
|
|
244
|
+
kernelSpawn: false,
|
|
245
|
+
websocket: false,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// Test kernel spawn with timeout
|
|
250
|
+
const testContext = await Promise.race([
|
|
251
|
+
this.jupyterServer.createContext({ language: "python" }),
|
|
252
|
+
new Promise<null>((_, reject) =>
|
|
253
|
+
setTimeout(() => reject(new Error("Kernel spawn timeout")), 5000)
|
|
254
|
+
),
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
if (testContext) {
|
|
258
|
+
checks.kernelSpawn = true;
|
|
259
|
+
|
|
260
|
+
// Test WebSocket by executing simple code
|
|
261
|
+
try {
|
|
262
|
+
const result = await Promise.race([
|
|
263
|
+
this.jupyterServer.executeCode(
|
|
264
|
+
testContext.id,
|
|
265
|
+
"print('health_check')"
|
|
266
|
+
),
|
|
267
|
+
new Promise<null>((_, reject) =>
|
|
268
|
+
setTimeout(() => reject(new Error("WebSocket timeout")), 3000)
|
|
269
|
+
),
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
checks.websocket = result !== null;
|
|
273
|
+
} catch {
|
|
274
|
+
// WebSocket test failed
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Clean up test context
|
|
278
|
+
try {
|
|
279
|
+
await this.jupyterServer.deleteContext(testContext.id);
|
|
280
|
+
} catch {
|
|
281
|
+
// Ignore cleanup errors
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.log("[JupyterService] Advanced health check failed:", error);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return checks;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Ensure Jupyter is initialized before proceeding
|
|
293
|
+
* This will wait for initialization to complete or fail
|
|
294
|
+
*/
|
|
295
|
+
private async ensureInitialized(timeoutMs: number = 30000): Promise<void> {
|
|
296
|
+
if (this.initialized) return;
|
|
297
|
+
|
|
298
|
+
// Start initialization if not already started
|
|
299
|
+
if (!this.initPromise) {
|
|
300
|
+
this.initPromise = this.initialize();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Wait for initialization with timeout
|
|
304
|
+
try {
|
|
305
|
+
await Promise.race([
|
|
306
|
+
this.initPromise,
|
|
307
|
+
new Promise<never>((_, reject) =>
|
|
308
|
+
setTimeout(
|
|
309
|
+
() =>
|
|
310
|
+
reject(new Error("Timeout waiting for Jupyter initialization")),
|
|
311
|
+
timeoutMs
|
|
312
|
+
)
|
|
313
|
+
),
|
|
314
|
+
]);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
// If it's a timeout and Jupyter is still initializing, throw a retryable error
|
|
317
|
+
if (
|
|
318
|
+
error instanceof Error &&
|
|
319
|
+
error.message.includes("Timeout") &&
|
|
320
|
+
!this.initError
|
|
321
|
+
) {
|
|
322
|
+
throw new JupyterNotReadyError(
|
|
323
|
+
"Jupyter is taking longer than expected to initialize. Please try again.",
|
|
324
|
+
{
|
|
325
|
+
retryAfter: 10,
|
|
326
|
+
progress: this.getInitializationProgress(),
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
// If initialization actually failed, throw the real error
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Jupyter initialization failed: ${
|
|
333
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
334
|
+
}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Create context - will wait for Jupyter if still initializing
|
|
341
|
+
*/
|
|
342
|
+
async createContext(req: CreateContextRequest): Promise<any> {
|
|
343
|
+
if (!this.initialized) {
|
|
344
|
+
console.log(
|
|
345
|
+
"[JupyterService] Context creation requested while Jupyter is initializing - waiting..."
|
|
346
|
+
);
|
|
347
|
+
const startWait = Date.now();
|
|
348
|
+
await this.ensureInitialized();
|
|
349
|
+
const waitTime = Date.now() - startWait;
|
|
350
|
+
console.log(
|
|
351
|
+
`[JupyterService] Jupyter ready after ${waitTime}ms wait - proceeding with context creation`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Use circuit breaker for context creation
|
|
356
|
+
return await this.circuitBreakers.contextCreation.execute(async () => {
|
|
357
|
+
return await this.jupyterServer.createContext(req);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Execute code - will wait for Jupyter if still initializing
|
|
363
|
+
*/
|
|
364
|
+
async executeCode(
|
|
365
|
+
contextId: string | undefined,
|
|
366
|
+
code: string,
|
|
367
|
+
language?: string
|
|
368
|
+
): Promise<Response> {
|
|
369
|
+
if (!this.initialized) {
|
|
370
|
+
console.log(
|
|
371
|
+
"[JupyterService] Code execution requested while Jupyter is initializing - waiting..."
|
|
372
|
+
);
|
|
373
|
+
const startWait = Date.now();
|
|
374
|
+
await this.ensureInitialized();
|
|
375
|
+
const waitTime = Date.now() - startWait;
|
|
376
|
+
console.log(
|
|
377
|
+
`[JupyterService] Jupyter ready after ${waitTime}ms wait - proceeding with code execution`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Use circuit breaker for code execution
|
|
382
|
+
return await this.circuitBreakers.codeExecution.execute(async () => {
|
|
383
|
+
return await this.jupyterServer.executeCode(contextId, code, language);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* List contexts with graceful degradation
|
|
389
|
+
*/
|
|
390
|
+
async listContexts(): Promise<any[]> {
|
|
391
|
+
if (!this.initialized) {
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
return await this.jupyterServer.listContexts();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Delete context - will wait for Jupyter if still initializing
|
|
399
|
+
*/
|
|
400
|
+
async deleteContext(contextId: string): Promise<void> {
|
|
401
|
+
if (!this.initialized) {
|
|
402
|
+
console.log(
|
|
403
|
+
"[JupyterService] Context deletion requested while Jupyter is initializing - waiting..."
|
|
404
|
+
);
|
|
405
|
+
const startWait = Date.now();
|
|
406
|
+
await this.ensureInitialized();
|
|
407
|
+
const waitTime = Date.now() - startWait;
|
|
408
|
+
console.log(
|
|
409
|
+
`[JupyterService] Jupyter ready after ${waitTime}ms wait - proceeding with context deletion`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Use circuit breaker for kernel communication
|
|
414
|
+
return await this.circuitBreakers.kernelCommunication.execute(async () => {
|
|
415
|
+
return await this.jupyterServer.deleteContext(contextId);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Shutdown the service
|
|
421
|
+
*/
|
|
422
|
+
async shutdown(): Promise<void> {
|
|
423
|
+
if (this.initialized) {
|
|
424
|
+
await this.jupyterServer.shutdown();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get initialization progress
|
|
430
|
+
*/
|
|
431
|
+
private getInitializationProgress(): number {
|
|
432
|
+
if (this.initialized) return 100;
|
|
433
|
+
if (!this.initPromise) return 0;
|
|
434
|
+
|
|
435
|
+
const elapsed = Date.now() - this.startTime;
|
|
436
|
+
const estimatedTotal = 20000; // 20 seconds estimated
|
|
437
|
+
return Math.min(95, Math.round((elapsed / estimatedTotal) * 100));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Error thrown when Jupyter is not ready yet
|
|
443
|
+
* This matches the interface of the SDK's JupyterNotReadyError
|
|
444
|
+
*/
|
|
445
|
+
export class JupyterNotReadyError extends Error {
|
|
446
|
+
public readonly retryAfter: number;
|
|
447
|
+
public readonly progress?: number;
|
|
448
|
+
|
|
449
|
+
constructor(
|
|
450
|
+
message: string,
|
|
451
|
+
options?: { retryAfter?: number; progress?: number }
|
|
452
|
+
) {
|
|
453
|
+
super(message);
|
|
454
|
+
this.name = "JupyterNotReadyError";
|
|
455
|
+
this.retryAfter = options?.retryAfter || 5;
|
|
456
|
+
this.progress = options?.progress;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Minimal Jupyter configuration focused on kernel-only usage
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
c = get_config() # noqa
|
|
6
|
+
|
|
7
|
+
# Disable all authentication - we handle security at container level
|
|
8
|
+
c.ServerApp.token = ''
|
|
9
|
+
c.ServerApp.password = ''
|
|
10
|
+
c.IdentityProvider.token = ''
|
|
11
|
+
c.ServerApp.allow_origin = '*'
|
|
12
|
+
c.ServerApp.allow_remote_access = True
|
|
13
|
+
c.ServerApp.disable_check_xsrf = True
|
|
14
|
+
c.ServerApp.allow_root = True
|
|
15
|
+
c.ServerApp.allow_credentials = True
|
|
16
|
+
|
|
17
|
+
# Also set NotebookApp settings for compatibility
|
|
18
|
+
c.NotebookApp.token = ''
|
|
19
|
+
c.NotebookApp.password = ''
|
|
20
|
+
c.NotebookApp.allow_origin = '*'
|
|
21
|
+
c.NotebookApp.allow_remote_access = True
|
|
22
|
+
c.NotebookApp.disable_check_xsrf = True
|
|
23
|
+
c.NotebookApp.allow_credentials = True
|
|
24
|
+
|
|
25
|
+
# Performance settings
|
|
26
|
+
c.ServerApp.iopub_data_rate_limit = 1000000000 # E2B uses 1GB/s
|
|
27
|
+
|
|
28
|
+
# Minimal logging
|
|
29
|
+
c.Application.log_level = 'ERROR'
|
|
30
|
+
|
|
31
|
+
# Disable browser
|
|
32
|
+
c.ServerApp.open_browser = False
|
|
33
|
+
|
|
34
|
+
# Optimize for container environment
|
|
35
|
+
c.ServerApp.ip = '0.0.0.0'
|
|
36
|
+
c.ServerApp.port = 8888
|
|
37
|
+
|
|
38
|
+
# Kernel optimizations
|
|
39
|
+
c.KernelManager.shutdown_wait_time = 0.0
|
|
40
|
+
c.MappingKernelManager.cull_idle_timeout = 0
|
|
41
|
+
c.MappingKernelManager.cull_interval = 0
|
|
42
|
+
|
|
43
|
+
# Disable terminals
|
|
44
|
+
c.ServerApp.terminals_enabled = False
|
|
45
|
+
|
|
46
|
+
# Disable all extensions to speed up startup
|
|
47
|
+
c.ServerApp.jpserver_extensions = {}
|
|
48
|
+
c.ServerApp.nbserver_extensions = {}
|
package/container_src/startup.sh
CHANGED
|
@@ -1,52 +1,84 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# Function to check if Jupyter is ready
|
|
4
|
+
check_jupyter_ready() {
|
|
5
|
+
# Check if API is responsive and kernelspecs are available
|
|
6
|
+
curl -s http://localhost:8888/api/kernelspecs > /dev/null 2>&1
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
# Function to notify Bun server that Jupyter is ready
|
|
10
|
+
notify_jupyter_ready() {
|
|
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
|
+
}
|
|
15
|
+
|
|
16
|
+
# Start Jupyter server in background
|
|
4
17
|
echo "[Startup] Starting Jupyter server..."
|
|
5
|
-
jupyter
|
|
6
|
-
--
|
|
7
|
-
--port=8888 \
|
|
8
|
-
--no-browser \
|
|
9
|
-
--allow-root \
|
|
10
|
-
--NotebookApp.token='' \
|
|
11
|
-
--NotebookApp.password='' \
|
|
12
|
-
--NotebookApp.allow_origin='*' \
|
|
13
|
-
--NotebookApp.disable_check_xsrf=True \
|
|
14
|
-
--NotebookApp.allow_remote_access=True \
|
|
15
|
-
--NotebookApp.allow_credentials=True \
|
|
18
|
+
jupyter server \
|
|
19
|
+
--config=/container-server/jupyter_config.py \
|
|
16
20
|
> /tmp/jupyter.log 2>&1 &
|
|
17
21
|
|
|
18
22
|
JUPYTER_PID=$!
|
|
19
23
|
|
|
20
|
-
#
|
|
21
|
-
echo "[Startup]
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
# Start Bun server immediately (parallel startup)
|
|
25
|
+
echo "[Startup] Starting Bun server..."
|
|
26
|
+
bun index.ts &
|
|
27
|
+
BUN_PID=$!
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
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:"
|
|
34
78
|
cat /tmp/jupyter.log
|
|
35
|
-
exit
|
|
79
|
+
# Don't exit - let Bun server continue in degraded mode
|
|
36
80
|
fi
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
done
|
|
42
|
-
|
|
43
|
-
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
|
|
44
|
-
echo "[Startup] ERROR: Jupyter failed to start within 30 seconds"
|
|
45
|
-
echo "[Startup] Jupyter logs:"
|
|
46
|
-
cat /tmp/jupyter.log
|
|
47
|
-
exit 1
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
# Start the main Bun server
|
|
51
|
-
echo "[Startup] Starting Bun server..."
|
|
52
|
-
exec bun index.ts
|
|
81
|
+
) &
|
|
82
|
+
|
|
83
|
+
# Wait for Bun server (main process)
|
|
84
|
+
wait $BUN_PID
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SecurityError,
|
|
3
|
+
logSecurityEvent,
|
|
4
|
+
sanitizeSandboxId,
|
|
5
|
+
validatePort
|
|
6
|
+
} from "./chunk-6UAWTJ5S.js";
|
|
1
7
|
import {
|
|
2
8
|
parseSSEStream
|
|
3
9
|
} from "./chunk-NNGBXDMY.js";
|
|
@@ -10,13 +16,7 @@ import {
|
|
|
10
16
|
} from "./chunk-FKBV7CZS.js";
|
|
11
17
|
import {
|
|
12
18
|
JupyterClient
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import {
|
|
15
|
-
SecurityError,
|
|
16
|
-
logSecurityEvent,
|
|
17
|
-
sanitizeSandboxId,
|
|
18
|
-
validatePort
|
|
19
|
-
} from "./chunk-6UAWTJ5S.js";
|
|
19
|
+
} from "./chunk-VTKZL632.js";
|
|
20
20
|
|
|
21
21
|
// src/sandbox.ts
|
|
22
22
|
import { Container, getContainer } from "@cloudflare/containers";
|
|
@@ -129,8 +129,8 @@ function getSandbox(ns, id) {
|
|
|
129
129
|
var Sandbox = class extends Container {
|
|
130
130
|
defaultPort = 3e3;
|
|
131
131
|
// Default port for the container's Bun server
|
|
132
|
-
sleepAfter = "
|
|
133
|
-
//
|
|
132
|
+
sleepAfter = "20m";
|
|
133
|
+
// Keep container warm for 20 minutes to avoid cold starts
|
|
134
134
|
client;
|
|
135
135
|
sandboxName = null;
|
|
136
136
|
codeInterpreter;
|
|
@@ -687,4 +687,4 @@ export {
|
|
|
687
687
|
proxyToSandbox,
|
|
688
688
|
isLocalhostPattern
|
|
689
689
|
};
|
|
690
|
-
//# sourceMappingURL=chunk-
|
|
690
|
+
//# sourceMappingURL=chunk-4KELYYKS.js.map
|