CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/jupyter/stateless-api/kernel.ts
Views: 687
1
import { kernel as createKernel } from "@cocalc/jupyter/kernel";
2
import type { JupyterKernelInterface } from "@cocalc/jupyter/types/project-interface";
3
import { run_cell, Limits } from "@cocalc/jupyter/nbgrader/jupyter-run";
4
import { mkdtemp, rm } from "fs/promises";
5
import { tmpdir } from "os";
6
import { join } from "path";
7
import getLogger from "@cocalc/backend/logger";
8
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
9
10
const log = getLogger("jupyter:stateless-api:kernel");
11
12
const DEFAULT_POOL_SIZE = 2;
13
const DEFAULT_POOL_TIMEOUT_S = 3600;
14
15
// When we idle timeout we always keep at least this many kernels around. We don't go to 0.
16
const MIN_POOL_SIZE = 1;
17
18
export default class Kernel {
19
private static pools: { [kernelName: string]: Kernel[] } = {};
20
private static last_active: { [kernelName: string]: number } = {};
21
22
private kernel?: JupyterKernelInterface;
23
private tempDir: string;
24
25
constructor(private kernelName: string) {
26
this.init = reuseInFlight(this.init.bind(this));
27
}
28
29
private static getPool(kernelName: string) {
30
let pool = Kernel.pools[kernelName];
31
if (pool == null) {
32
pool = Kernel.pools[kernelName] = [];
33
}
34
return pool;
35
}
36
37
// Set a timeout for a given kernel pool (for a specifically named kernel)
38
// to determine when to clear it if no requests have been made.
39
private static setIdleTimeout(kernelName: string, timeout_s: number) {
40
if (!timeout_s) {
41
// 0 = no timeout
42
return;
43
}
44
const now = Date.now();
45
Kernel.last_active[kernelName] = now;
46
setTimeout(() => {
47
if (Kernel.last_active[kernelName] > now) {
48
// kernel was requested after now.
49
return;
50
}
51
// No recent request for kernelName.
52
// Keep at least MIN_POOL_SIZE in Kernel.pools[kernelName]. I.e.,
53
// instead of closing and deleting everything, we just want to
54
// shrink the pool to MIN_POOL_SIZE.
55
// no request for kernelName, so we clear them from the pool
56
const poolToShrink = Kernel.pools[kernelName] ?? [];
57
if (poolToShrink.length > MIN_POOL_SIZE) {
58
// check if pool needs shrinking
59
// calculate how many to close
60
const numToClose = poolToShrink.length - MIN_POOL_SIZE;
61
for (let i = 0; i < numToClose; i++) {
62
poolToShrink[i].close(); // close oldest kernels first
63
}
64
// update pool to have only the most recent kernels
65
Kernel.pools[kernelName] = poolToShrink.slice(numToClose);
66
}
67
}, (timeout_s ?? DEFAULT_POOL_TIMEOUT_S) * 1000);
68
}
69
70
static async getFromPool(
71
kernelName: string,
72
{
73
size = DEFAULT_POOL_SIZE,
74
timeout_s = DEFAULT_POOL_TIMEOUT_S,
75
}: { size?: number; timeout_s?: number } = {}
76
): Promise<Kernel> {
77
this.setIdleTimeout(kernelName, timeout_s);
78
const pool = Kernel.getPool(kernelName);
79
let i = 1;
80
while (pool.length <= size) {
81
// <= since going to remove one below
82
const k = new Kernel(kernelName);
83
// we cause this kernel to get init'd soon, but NOT immediately, since starting
84
// several at once just makes them all take much longer exactly when the user
85
// most wants to use their new kernel
86
setTimeout(async () => {
87
try {
88
await k.init();
89
} catch (err) {
90
log.debug("Failed to pre-init Jupyter kernel -- ", kernelName, err);
91
}
92
}, 3000 * i); // stagger startup by a few seconds, though kernels that are needed will start ASAP.
93
i += 1;
94
pool.push(k);
95
}
96
const k = pool.shift() as Kernel;
97
// it's ok to call again due to reuseInFlight and that no-op after init.
98
await k.init();
99
return k;
100
}
101
102
private async init() {
103
if (this.kernel != null) {
104
// already initialized
105
return;
106
}
107
this.tempDir = await mkdtemp(join(tmpdir(), "cocalc"));
108
const path = `${this.tempDir}/execute.ipynb`;
109
// TODO: make this configurable as part of the API call
110
// I'm having a lot of trouble with this for now.
111
// -n = max open files
112
// -f = max bytes allowed to *write* to disk
113
// -t = max cputime is 30 seconds
114
// -v = max virtual memory usage to 3GB
115
this.kernel = createKernel({
116
name: this.kernelName,
117
path,
118
// ulimit: `-n 1000 -f 10485760 -t 30 -v 3000000`,
119
});
120
await this.kernel.ensure_running();
121
await this.kernel.execute_code_now({ code: "" });
122
}
123
124
async execute(
125
code: string,
126
limits: Limits = {
127
timeout_ms: 30000,
128
timeout_ms_per_cell: 30000,
129
max_output: 5000000,
130
max_output_per_cell: 1000000,
131
start_time: Date.now(),
132
total_output: 0,
133
}
134
) {
135
if (this.kernel == null) {
136
throw Error("kernel already closed");
137
}
138
139
if (limits.total_output == null) {
140
limits.total_output = 0;
141
}
142
const cell = { cell_type: "code", source: [code], outputs: [] };
143
await run_cell(this.kernel, limits, cell);
144
return cell.outputs;
145
}
146
147
async chdir(path: string) {
148
if (this.kernel == null) return;
149
await this.kernel.chdir(path);
150
}
151
152
async returnToPool(): Promise<void> {
153
if (this.kernel == null) {
154
throw Error("kernel already closed");
155
}
156
const pool = Kernel.getPool(this.kernelName);
157
pool.push(this);
158
}
159
160
async close() {
161
if (this.kernel == null) return;
162
try {
163
await this.kernel.close();
164
} catch (err) {
165
log.warn("Error closing kernel", err);
166
} finally {
167
delete this.kernel;
168
}
169
try {
170
await rm(this.tempDir, { force: true, recursive: true });
171
} catch (err) {
172
log.warn("Error cleaning up temporary directory", err);
173
}
174
}
175
}
176
177