@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
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
IStreamMsg
|
|
8
|
-
} from "@jupyterlab/services/lib/kernel/messages";
|
|
9
|
-
import {
|
|
10
|
-
isDisplayDataMsg,
|
|
11
|
-
isErrorMsg,
|
|
12
|
-
isExecuteResultMsg,
|
|
13
|
-
isStreamMsg
|
|
14
|
-
} from "@jupyterlab/services/lib/kernel/messages";
|
|
1
|
+
import {
|
|
2
|
+
type Kernel,
|
|
3
|
+
KernelManager,
|
|
4
|
+
ServerConnection,
|
|
5
|
+
} from "@jupyterlab/services";
|
|
6
|
+
import type { IIOPubMessage } from "@jupyterlab/services/lib/kernel/messages";
|
|
15
7
|
import { v4 as uuidv4 } from "uuid";
|
|
16
|
-
import type { ExecutionResult } from "./mime-processor";
|
|
17
8
|
import { processJupyterMessage } from "./mime-processor";
|
|
18
9
|
|
|
19
10
|
export interface JupyterContext {
|
|
@@ -23,6 +14,8 @@ export interface JupyterContext {
|
|
|
23
14
|
cwd: string;
|
|
24
15
|
createdAt: Date;
|
|
25
16
|
lastUsed: Date;
|
|
17
|
+
pooled?: boolean; // Track if this context is from the pool
|
|
18
|
+
inUse?: boolean; // Track if pooled context is currently in use
|
|
26
19
|
}
|
|
27
20
|
|
|
28
21
|
export interface CreateContextRequest {
|
|
@@ -38,10 +31,19 @@ export interface ExecuteCodeRequest {
|
|
|
38
31
|
env_vars?: Record<string, string>;
|
|
39
32
|
}
|
|
40
33
|
|
|
34
|
+
interface ContextPool {
|
|
35
|
+
available: JupyterContext[];
|
|
36
|
+
inUse: Set<string>; // context IDs currently in use
|
|
37
|
+
minSize: number;
|
|
38
|
+
maxSize: number;
|
|
39
|
+
warming: boolean; // Track if pool is currently being warmed
|
|
40
|
+
}
|
|
41
|
+
|
|
41
42
|
export class JupyterServer {
|
|
42
43
|
private kernelManager: KernelManager;
|
|
43
44
|
private contexts = new Map<string, JupyterContext>();
|
|
44
45
|
private defaultContexts = new Map<string, string>(); // language -> context_id
|
|
46
|
+
private contextPools = new Map<string, ContextPool>(); // language -> pool
|
|
45
47
|
|
|
46
48
|
constructor() {
|
|
47
49
|
// Configure connection to local Jupyter server
|
|
@@ -53,9 +55,9 @@ export class JupyterServer {
|
|
|
53
55
|
appendToken: false,
|
|
54
56
|
init: {
|
|
55
57
|
headers: {
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
}
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
59
61
|
});
|
|
60
62
|
|
|
61
63
|
this.kernelManager = new KernelManager({ serverSettings });
|
|
@@ -65,12 +67,172 @@ export class JupyterServer {
|
|
|
65
67
|
await this.kernelManager.ready;
|
|
66
68
|
console.log("[JupyterServer] Kernel manager initialized");
|
|
67
69
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
// Don't create default context during initialization - use lazy loading instead
|
|
71
|
+
|
|
72
|
+
// Initialize pools for common languages (but don't warm them yet)
|
|
73
|
+
this.initializePool("python", 0, 3);
|
|
74
|
+
this.initializePool("javascript", 0, 2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Initialize a context pool for a specific language
|
|
79
|
+
*/
|
|
80
|
+
private initializePool(language: string, minSize = 0, maxSize = 5) {
|
|
81
|
+
const pool: ContextPool = {
|
|
82
|
+
available: [],
|
|
83
|
+
inUse: new Set(),
|
|
84
|
+
minSize,
|
|
85
|
+
maxSize,
|
|
86
|
+
warming: false,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this.contextPools.set(language, pool);
|
|
90
|
+
|
|
91
|
+
// Pre-warm contexts in background if minSize > 0
|
|
92
|
+
if (minSize > 0) {
|
|
93
|
+
setTimeout(() => this.warmPool(language, minSize), 0);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Enable pool warming for a language (called after Jupyter is ready)
|
|
99
|
+
*/
|
|
100
|
+
async enablePoolWarming(language: string, minSize: number) {
|
|
101
|
+
const pool = this.contextPools.get(language);
|
|
102
|
+
if (!pool) {
|
|
103
|
+
this.initializePool(language, minSize, 3);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Update min size and warm if needed
|
|
108
|
+
pool.minSize = minSize;
|
|
109
|
+
const toWarm = minSize - pool.available.length;
|
|
110
|
+
if (toWarm > 0) {
|
|
111
|
+
await this.warmPool(language, toWarm);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Pre-warm a pool with the specified number of contexts
|
|
117
|
+
*/
|
|
118
|
+
private async warmPool(language: string, count: number) {
|
|
119
|
+
const pool = this.contextPools.get(language);
|
|
120
|
+
if (!pool || pool.warming) return;
|
|
121
|
+
|
|
122
|
+
pool.warming = true;
|
|
123
|
+
console.log(`[JupyterServer] Pre-warming ${count} ${language} contexts`);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const promises: Promise<JupyterContext>[] = [];
|
|
127
|
+
for (let i = 0; i < count; i++) {
|
|
128
|
+
promises.push(this.createPooledContext(language));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const contexts = await Promise.all(promises);
|
|
132
|
+
pool.available.push(...contexts);
|
|
133
|
+
console.log(
|
|
134
|
+
`[JupyterServer] Pre-warmed ${contexts.length} ${language} contexts`
|
|
135
|
+
);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(
|
|
138
|
+
`[JupyterServer] Error pre-warming ${language} pool:`,
|
|
139
|
+
error
|
|
140
|
+
);
|
|
141
|
+
} finally {
|
|
142
|
+
pool.warming = false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a context specifically for the pool
|
|
148
|
+
*/
|
|
149
|
+
private async createPooledContext(language: string): Promise<JupyterContext> {
|
|
150
|
+
const context = await this.createContext({ language });
|
|
151
|
+
context.pooled = true;
|
|
152
|
+
context.inUse = false;
|
|
153
|
+
return context;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get a context from the pool or create a new one
|
|
158
|
+
*/
|
|
159
|
+
private async getPooledContext(
|
|
160
|
+
language: string,
|
|
161
|
+
cwd?: string,
|
|
162
|
+
envVars?: Record<string, string>
|
|
163
|
+
): Promise<JupyterContext | null> {
|
|
164
|
+
const pool = this.contextPools.get(language);
|
|
165
|
+
if (!pool) return null;
|
|
166
|
+
|
|
167
|
+
// Find an available context in the pool
|
|
168
|
+
const availableContext = pool.available.find((ctx) => !ctx.inUse);
|
|
169
|
+
|
|
170
|
+
if (availableContext) {
|
|
171
|
+
// Mark as in use
|
|
172
|
+
availableContext.inUse = true;
|
|
173
|
+
pool.inUse.add(availableContext.id);
|
|
174
|
+
|
|
175
|
+
// Remove from available list
|
|
176
|
+
pool.available = pool.available.filter(
|
|
177
|
+
(ctx) => ctx.id !== availableContext.id
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Update context properties if needed
|
|
181
|
+
if (cwd && cwd !== availableContext.cwd) {
|
|
182
|
+
await this.changeWorkingDirectory(availableContext, cwd);
|
|
183
|
+
availableContext.cwd = cwd;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (envVars) {
|
|
187
|
+
await this.setEnvironmentVariables(availableContext, envVars);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
availableContext.lastUsed = new Date();
|
|
191
|
+
console.log(
|
|
192
|
+
`[JupyterServer] Reusing pooled ${language} context ${availableContext.id}`
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Warm another context in background if we're below minSize
|
|
196
|
+
if (pool.available.length < pool.minSize && !pool.warming) {
|
|
197
|
+
setTimeout(() => this.warmPool(language, 1), 0);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return availableContext;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// No available context, check if we can create a new one
|
|
204
|
+
if (pool.inUse.size < pool.maxSize) {
|
|
205
|
+
console.log(
|
|
206
|
+
`[JupyterServer] No pooled ${language} context available, creating new one`
|
|
207
|
+
);
|
|
208
|
+
return null; // Let the caller create a new context
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Pool is at max capacity
|
|
71
212
|
console.log(
|
|
72
|
-
|
|
73
|
-
|
|
213
|
+
`[JupyterServer] ${language} context pool at max capacity (${pool.maxSize})`
|
|
214
|
+
);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Release a pooled context back to the pool
|
|
220
|
+
*/
|
|
221
|
+
private releasePooledContext(context: JupyterContext) {
|
|
222
|
+
if (!context.pooled) return;
|
|
223
|
+
|
|
224
|
+
const pool = this.contextPools.get(context.language);
|
|
225
|
+
if (!pool) return;
|
|
226
|
+
|
|
227
|
+
// Mark as not in use
|
|
228
|
+
context.inUse = false;
|
|
229
|
+
pool.inUse.delete(context.id);
|
|
230
|
+
|
|
231
|
+
// Add back to available list
|
|
232
|
+
pool.available.push(context);
|
|
233
|
+
|
|
234
|
+
console.log(
|
|
235
|
+
`[JupyterServer] Released ${context.language} context ${context.id} back to pool`
|
|
74
236
|
);
|
|
75
237
|
}
|
|
76
238
|
|
|
@@ -78,6 +240,19 @@ export class JupyterServer {
|
|
|
78
240
|
const language = req.language || "python";
|
|
79
241
|
const cwd = req.cwd || "/workspace";
|
|
80
242
|
|
|
243
|
+
// Try to get a context from the pool first
|
|
244
|
+
const pooledContext = await this.getPooledContext(
|
|
245
|
+
language,
|
|
246
|
+
cwd,
|
|
247
|
+
req.envVars
|
|
248
|
+
);
|
|
249
|
+
if (pooledContext) {
|
|
250
|
+
// Add to active contexts map
|
|
251
|
+
this.contexts.set(pooledContext.id, pooledContext);
|
|
252
|
+
return pooledContext;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Create a new context if pool didn't provide one
|
|
81
256
|
const kernelModel = await this.kernelManager.startNew({
|
|
82
257
|
name: this.getKernelName(language),
|
|
83
258
|
});
|
|
@@ -108,6 +283,27 @@ export class JupyterServer {
|
|
|
108
283
|
return context;
|
|
109
284
|
}
|
|
110
285
|
|
|
286
|
+
private async getOrCreateDefaultContext(
|
|
287
|
+
language: string
|
|
288
|
+
): Promise<JupyterContext | undefined> {
|
|
289
|
+
// Check if we already have a default context for this language
|
|
290
|
+
const defaultContextId = this.defaultContexts.get(language);
|
|
291
|
+
if (defaultContextId) {
|
|
292
|
+
const context = this.contexts.get(defaultContextId);
|
|
293
|
+
if (context) {
|
|
294
|
+
return context;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Create new default context lazily
|
|
299
|
+
console.log(
|
|
300
|
+
`[JupyterServer] Creating default ${language} context on first use`
|
|
301
|
+
);
|
|
302
|
+
const context = await this.createContext({ language });
|
|
303
|
+
this.defaultContexts.set(language, context.id);
|
|
304
|
+
return context;
|
|
305
|
+
}
|
|
306
|
+
|
|
111
307
|
async executeCode(
|
|
112
308
|
contextId: string | undefined,
|
|
113
309
|
code: string,
|
|
@@ -126,24 +322,10 @@ export class JupyterServer {
|
|
|
126
322
|
}
|
|
127
323
|
);
|
|
128
324
|
}
|
|
129
|
-
} else if (language) {
|
|
130
|
-
// Use default context for the language
|
|
131
|
-
const defaultContextId = this.defaultContexts.get(language);
|
|
132
|
-
if (defaultContextId) {
|
|
133
|
-
context = this.contexts.get(defaultContextId);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Create new default context if needed
|
|
137
|
-
if (!context) {
|
|
138
|
-
context = await this.createContext({ language });
|
|
139
|
-
this.defaultContexts.set(language, context.id);
|
|
140
|
-
}
|
|
141
325
|
} else {
|
|
142
|
-
// Use default
|
|
143
|
-
const
|
|
144
|
-
context =
|
|
145
|
-
? this.contexts.get(pythonContextId)
|
|
146
|
-
: undefined;
|
|
326
|
+
// Use or create default context for the language
|
|
327
|
+
const lang = language || "python";
|
|
328
|
+
context = await this.getOrCreateDefaultContext(lang);
|
|
147
329
|
}
|
|
148
330
|
|
|
149
331
|
if (!context) {
|
|
@@ -304,10 +486,7 @@ export class JupyterServer {
|
|
|
304
486
|
throw new Error(`Context ${contextId} not found`);
|
|
305
487
|
}
|
|
306
488
|
|
|
307
|
-
//
|
|
308
|
-
await context.connection.shutdown();
|
|
309
|
-
|
|
310
|
-
// Remove from maps
|
|
489
|
+
// Remove from active contexts map
|
|
311
490
|
this.contexts.delete(contextId);
|
|
312
491
|
|
|
313
492
|
// Remove from default contexts if it was a default
|
|
@@ -317,10 +496,18 @@ export class JupyterServer {
|
|
|
317
496
|
break;
|
|
318
497
|
}
|
|
319
498
|
}
|
|
499
|
+
|
|
500
|
+
// If it's a pooled context, release it back to the pool
|
|
501
|
+
if (context.pooled) {
|
|
502
|
+
this.releasePooledContext(context);
|
|
503
|
+
} else {
|
|
504
|
+
// Only shutdown non-pooled contexts
|
|
505
|
+
await context.connection.shutdown();
|
|
506
|
+
}
|
|
320
507
|
}
|
|
321
508
|
|
|
322
509
|
async shutdown() {
|
|
323
|
-
// Shutdown all
|
|
510
|
+
// Shutdown all active contexts
|
|
324
511
|
for (const context of this.contexts.values()) {
|
|
325
512
|
try {
|
|
326
513
|
await context.connection.shutdown();
|
|
@@ -329,8 +516,64 @@ export class JupyterServer {
|
|
|
329
516
|
}
|
|
330
517
|
}
|
|
331
518
|
|
|
519
|
+
// Shutdown all pooled contexts
|
|
520
|
+
for (const pool of this.contextPools.values()) {
|
|
521
|
+
for (const context of pool.available) {
|
|
522
|
+
try {
|
|
523
|
+
await context.connection.shutdown();
|
|
524
|
+
} catch (error) {
|
|
525
|
+
console.error(
|
|
526
|
+
"[JupyterServer] Error shutting down pooled kernel:",
|
|
527
|
+
error
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
332
533
|
this.contexts.clear();
|
|
333
534
|
this.defaultContexts.clear();
|
|
535
|
+
this.contextPools.clear();
|
|
334
536
|
}
|
|
335
|
-
}
|
|
336
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Get pool statistics for monitoring
|
|
540
|
+
*/
|
|
541
|
+
async getPoolStats(): Promise<
|
|
542
|
+
Record<
|
|
543
|
+
string,
|
|
544
|
+
{
|
|
545
|
+
available: number;
|
|
546
|
+
inUse: number;
|
|
547
|
+
total: number;
|
|
548
|
+
minSize: number;
|
|
549
|
+
maxSize: number;
|
|
550
|
+
warming: boolean;
|
|
551
|
+
}
|
|
552
|
+
>
|
|
553
|
+
> {
|
|
554
|
+
const stats: Record<
|
|
555
|
+
string,
|
|
556
|
+
{
|
|
557
|
+
available: number;
|
|
558
|
+
inUse: number;
|
|
559
|
+
total: number;
|
|
560
|
+
minSize: number;
|
|
561
|
+
maxSize: number;
|
|
562
|
+
warming: boolean;
|
|
563
|
+
}
|
|
564
|
+
> = {};
|
|
565
|
+
|
|
566
|
+
for (const [language, pool] of this.contextPools.entries()) {
|
|
567
|
+
stats[language] = {
|
|
568
|
+
available: pool.available.length,
|
|
569
|
+
inUse: pool.inUse.size,
|
|
570
|
+
total: pool.available.length + pool.inUse.size,
|
|
571
|
+
minSize: pool.minSize,
|
|
572
|
+
maxSize: pool.maxSize,
|
|
573
|
+
warming: pool.warming,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return stats;
|
|
578
|
+
}
|
|
579
|
+
}
|