Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/jupyter/stateless-api/kernel.ts
Views: 687
import { kernel as createKernel } from "@cocalc/jupyter/kernel";1import type { JupyterKernelInterface } from "@cocalc/jupyter/types/project-interface";2import { run_cell, Limits } from "@cocalc/jupyter/nbgrader/jupyter-run";3import { mkdtemp, rm } from "fs/promises";4import { tmpdir } from "os";5import { join } from "path";6import getLogger from "@cocalc/backend/logger";7import { reuseInFlight } from "@cocalc/util/reuse-in-flight";89const log = getLogger("jupyter:stateless-api:kernel");1011const DEFAULT_POOL_SIZE = 2;12const DEFAULT_POOL_TIMEOUT_S = 3600;1314// When we idle timeout we always keep at least this many kernels around. We don't go to 0.15const MIN_POOL_SIZE = 1;1617export default class Kernel {18private static pools: { [kernelName: string]: Kernel[] } = {};19private static last_active: { [kernelName: string]: number } = {};2021private kernel?: JupyterKernelInterface;22private tempDir: string;2324constructor(private kernelName: string) {25this.init = reuseInFlight(this.init.bind(this));26}2728private static getPool(kernelName: string) {29let pool = Kernel.pools[kernelName];30if (pool == null) {31pool = Kernel.pools[kernelName] = [];32}33return pool;34}3536// Set a timeout for a given kernel pool (for a specifically named kernel)37// to determine when to clear it if no requests have been made.38private static setIdleTimeout(kernelName: string, timeout_s: number) {39if (!timeout_s) {40// 0 = no timeout41return;42}43const now = Date.now();44Kernel.last_active[kernelName] = now;45setTimeout(() => {46if (Kernel.last_active[kernelName] > now) {47// kernel was requested after now.48return;49}50// No recent request for kernelName.51// Keep at least MIN_POOL_SIZE in Kernel.pools[kernelName]. I.e.,52// instead of closing and deleting everything, we just want to53// shrink the pool to MIN_POOL_SIZE.54// no request for kernelName, so we clear them from the pool55const poolToShrink = Kernel.pools[kernelName] ?? [];56if (poolToShrink.length > MIN_POOL_SIZE) {57// check if pool needs shrinking58// calculate how many to close59const numToClose = poolToShrink.length - MIN_POOL_SIZE;60for (let i = 0; i < numToClose; i++) {61poolToShrink[i].close(); // close oldest kernels first62}63// update pool to have only the most recent kernels64Kernel.pools[kernelName] = poolToShrink.slice(numToClose);65}66}, (timeout_s ?? DEFAULT_POOL_TIMEOUT_S) * 1000);67}6869static async getFromPool(70kernelName: string,71{72size = DEFAULT_POOL_SIZE,73timeout_s = DEFAULT_POOL_TIMEOUT_S,74}: { size?: number; timeout_s?: number } = {}75): Promise<Kernel> {76this.setIdleTimeout(kernelName, timeout_s);77const pool = Kernel.getPool(kernelName);78let i = 1;79while (pool.length <= size) {80// <= since going to remove one below81const k = new Kernel(kernelName);82// we cause this kernel to get init'd soon, but NOT immediately, since starting83// several at once just makes them all take much longer exactly when the user84// most wants to use their new kernel85setTimeout(async () => {86try {87await k.init();88} catch (err) {89log.debug("Failed to pre-init Jupyter kernel -- ", kernelName, err);90}91}, 3000 * i); // stagger startup by a few seconds, though kernels that are needed will start ASAP.92i += 1;93pool.push(k);94}95const k = pool.shift() as Kernel;96// it's ok to call again due to reuseInFlight and that no-op after init.97await k.init();98return k;99}100101private async init() {102if (this.kernel != null) {103// already initialized104return;105}106this.tempDir = await mkdtemp(join(tmpdir(), "cocalc"));107const path = `${this.tempDir}/execute.ipynb`;108// TODO: make this configurable as part of the API call109// I'm having a lot of trouble with this for now.110// -n = max open files111// -f = max bytes allowed to *write* to disk112// -t = max cputime is 30 seconds113// -v = max virtual memory usage to 3GB114this.kernel = createKernel({115name: this.kernelName,116path,117// ulimit: `-n 1000 -f 10485760 -t 30 -v 3000000`,118});119await this.kernel.ensure_running();120await this.kernel.execute_code_now({ code: "" });121}122123async execute(124code: string,125limits: Limits = {126timeout_ms: 30000,127timeout_ms_per_cell: 30000,128max_output: 5000000,129max_output_per_cell: 1000000,130start_time: Date.now(),131total_output: 0,132}133) {134if (this.kernel == null) {135throw Error("kernel already closed");136}137138if (limits.total_output == null) {139limits.total_output = 0;140}141const cell = { cell_type: "code", source: [code], outputs: [] };142await run_cell(this.kernel, limits, cell);143return cell.outputs;144}145146async chdir(path: string) {147if (this.kernel == null) return;148await this.kernel.chdir(path);149}150151async returnToPool(): Promise<void> {152if (this.kernel == null) {153throw Error("kernel already closed");154}155const pool = Kernel.getPool(this.kernelName);156pool.push(this);157}158159async close() {160if (this.kernel == null) return;161try {162await this.kernel.close();163} catch (err) {164log.warn("Error closing kernel", err);165} finally {166delete this.kernel;167}168try {169await rm(this.tempDir, { force: true, recursive: true });170} catch (err) {171log.warn("Error cleaning up temporary directory", err);172}173}174}175176177