@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.
@@ -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 = {}
@@ -1,52 +1,84 @@
1
1
  #!/bin/bash
2
2
 
3
- # Start Jupyter notebook server in background
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 notebook \
6
- --ip=0.0.0.0 \
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
- # Wait for Jupyter to be ready
21
- echo "[Startup] Waiting for Jupyter to become ready..."
22
- MAX_ATTEMPTS=30
23
- ATTEMPT=0
24
+ # Start Bun server immediately (parallel startup)
25
+ echo "[Startup] Starting Bun server..."
26
+ bun index.ts &
27
+ BUN_PID=$!
24
28
 
25
- while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
26
- if curl -s http://localhost:8888/api > /dev/null 2>&1; then
27
- echo "[Startup] Jupyter server is ready!"
28
- break
29
- fi
30
-
31
- # Check if Jupyter process is still running
32
- if ! kill -0 $JUPYTER_PID 2>/dev/null; then
33
- echo "[Startup] ERROR: Jupyter process died. Check /tmp/jupyter.log for details"
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 1
79
+ # Don't exit - let Bun server continue in degraded mode
36
80
  fi
37
-
38
- ATTEMPT=$((ATTEMPT + 1))
39
- echo "[Startup] Waiting for Jupyter... (attempt $ATTEMPT/$MAX_ATTEMPTS)"
40
- sleep 1
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-SYMWNYWA.js";
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 = "3m";
133
- // Sleep the sandbox if no requests are made in this timeframe
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-IATLC32Y.js.map
690
+ //# sourceMappingURL=chunk-4KELYYKS.js.map