Path: blob/master/src/packages/frontend/course/compute/actions.ts
5899 views
import type { CourseActions } from "../actions";1import { cloneConfiguration } from "@cocalc/frontend/compute/clone";2import type { Unit } from "../store";3import { reuseInFlight } from "@cocalc/util/reuse-in-flight";4import type { ComputeServerConfig } from "../types";5import { merge } from "lodash";6import type { Command } from "./students";7import { getUnitId, MAX_PARALLEL_TASKS } from "./util";8import {9computeServerAction,10createServer,11deleteServer,12getServersById,13setServerOwner,14} from "@cocalc/frontend/compute/api";15import { webapp_client } from "@cocalc/frontend/webapp-client";16import { map as awaitMap } from "awaiting";17import { getComputeServers } from "./synctable";18import { join } from "path";19import {20computeServerManager,21type ComputeServerManager,22} from "@cocalc/conat/compute/manager";2324declare var DEBUG: boolean;2526// const log = (..._args)=>{};27const log = DEBUG ? console.log : (..._args) => {};2829export class ComputeActions {30private course_actions: CourseActions;31private debugComputeServer?: {32project_id: string;33compute_server_id: number;34};3536constructor(course_actions: CourseActions) {37this.course_actions = course_actions;38}3940private getStore = () => {41const store = this.course_actions.get_store();42if (store == null) {43throw Error("no store");44}45return store;46};4748private getUnit = (49unit_id: string,50): {51unit: Unit;52table: "assignments" | "handouts";53} => {54// this code below is reasonable since the id is a random uuidv4, so no55// overlap between assignments and handouts in practice.56const assignment = this.course_actions.syncdb.get_one({57assignment_id: unit_id,58table: "assignments",59});60if (assignment != null) {61return { unit: assignment as unknown as Unit, table: "assignments" };62}63const handout = this.course_actions.syncdb.get_one({64handout_id: unit_id,65table: "handouts",66});67if (handout != null) {68return { unit: handout as unknown as Unit, table: "handouts" };69}70throw Error(`no assignment or handout with id '${unit_id}'`);71};7273setComputeServerConfig = ({74unit_id,75compute_server,76}: {77unit_id: string;78compute_server: ComputeServerConfig;79}) => {80let { table, unit } = this.getUnit(unit_id);81const obj = { ...unit.toJS(), table };82obj.compute_server = merge(obj.compute_server, compute_server);83this.course_actions.set(obj, true, true);84};8586// Create and compute server associated to a given assignment or handout87// for a specific student. Does nothing if (1) the compute server already88// exists, or (2) no compute server is configured for the given assignment.89private createComputeServer = reuseInFlight(90async ({91student_id,92unit_id,93}: {94student_id: string;95unit_id: string;96}): Promise<number | undefined> => {97// what compute server is configured for this assignment or handout?98const { unit } = this.getUnit(unit_id);99const compute_server = unit.get("compute_server");100if (compute_server == null) {101log("createComputeServer -- nothing to do - nothing configured.", {102student_id,103});104return;105}106const course_server_id = compute_server.get("server_id");107if (!course_server_id) {108log(109"createComputeServer -- nothing to do - compute server not configured for this unit.",110{111student_id,112},113);114return;115}116const cur_id = compute_server.getIn([117"students",118student_id,119"server_id",120]);121if (cur_id) {122log("compute server already exists", { cur_id, student_id });123return cur_id;124}125const store = this.getStore();126const course_project_id = store.get("course_project_id");127let student_project_id = store.get_student_project_id(student_id);128if (!student_project_id) {129student_project_id =130await this.course_actions.student_projects.create_student_project(131student_id,132);133}134if (!student_project_id) {135throw Error("unable to create the student's project");136}137138// Is there already a compute server in the target project139// with this course_server_id and course_project_id? If so,140// we use that one, since we don't want to have multiple copies141// of the *same* source compute server for multiple handouts142// or assignments.143const v = (144await getComputeServers({145project_id: student_project_id,146course_project_id,147course_server_id,148fields: ["id", "deleted"],149})150).filter(({ deleted }) => !deleted);151152let server_id;153if (v.length > 0) {154// compute server already exists -- use it155server_id = v[0].id;156} else {157// create new compute server158const server = await cloneConfiguration({159id: course_server_id,160noChange: true,161});162const studentServer = {163...server,164project_id: student_project_id,165course_server_id,166course_project_id,167};168// we must enable allowCollaboratorControl since it's needed for the169// student to start/stop the compute server.170studentServer.configuration.allowCollaboratorControl = true;171server_id = await createServer(studentServer);172}173174this.setComputeServerConfig({175unit_id,176compute_server: { students: { [student_id]: { server_id } } },177});178return server_id;179},180);181182// returns GLOBAL id of compute server for the given unit, or undefined if one isn't configured.183getComputeServerId = ({ unit, student_id }): number | undefined => {184return unit.getIn([185"compute_server",186"students",187student_id,188"server_id",189]) as number | undefined;190};191192computeServerCommand = async ({193command,194unit,195student_id,196}: {197command: Command;198unit: Unit;199student_id: string;200}) => {201if (command == "create") {202const unit_id = getUnitId(unit);203await this.createComputeServer({ student_id, unit_id });204return;205}206const server_id = this.getComputeServerId({ unit, student_id });207if (!server_id) {208throw Error("compute server doesn't exist");209}210switch (command) {211case "transfer":212const student = this.getStore()?.get_student(student_id);213const new_account_id = student?.get("account_id");214if (!new_account_id) {215throw Error("student does not have an account yet");216}217await setServerOwner({ id: server_id, new_account_id });218return;219case "start":220case "stop":221case "reboot":222case "deprovision":223await computeServerAction({ id: server_id, action: command });224return;225case "delete":226const unit_id = getUnitId(unit);227this.setComputeServerConfig({228unit_id,229compute_server: { students: { [student_id]: { server_id: 0 } } },230});231// only actually delete the server from the backend if no other232// units also refer to it:233if (234this.getUnitsUsingComputeServer({ student_id, server_id }).length == 0235) {236await deleteServer(server_id);237}238return;239case "transfer":240// todo241default:242throw Error(`command '${command}' not implemented`);243}244};245246private getUnitIds = () => {247const store = this.getStore();248if (store == null) {249throw Error("store must be defined");250}251return store.get_assignment_ids().concat(store.get_handout_ids());252};253254private getUnitsUsingComputeServer = ({255student_id,256server_id,257}: {258student_id: string;259server_id: number;260}): string[] => {261const v: string[] = [];262for (const id of this.getUnitIds()) {263const { unit } = this.getUnit(id);264if (265unit.getIn(["compute_server", "students", student_id, "server_id"]) ==266server_id267) {268v.push(id);269}270}271return v;272};273274private getDebugComputeServer = reuseInFlight(async () => {275if (this.debugComputeServer == null) {276const compute_server_id = 1;277const project_id = (278await getServersById({279ids: [compute_server_id],280fields: ["project_id"],281})282)[0].project_id as string;283this.debugComputeServer = { compute_server_id, project_id };284}285return this.debugComputeServer;286});287288private runTerminalCommandOneStudent = async ({289unit,290student_id,291...terminalOptions292}) => {293const store = this.getStore();294let project_id = store.get_student_project_id(student_id);295if (!project_id) {296throw Error("student project doesn't exist");297}298let compute_server_id = this.getComputeServerId({ unit, student_id });299if (!compute_server_id) {300throw Error("compute server doesn't exist");301}302if (DEBUG) {303log(304"runTerminalCommandOneStudent: in DEBUG mode, so actually using debug compute server",305);306({ compute_server_id, project_id } = await this.getDebugComputeServer());307}308309return await webapp_client.project_client.exec({310...terminalOptions,311project_id,312compute_server_id,313});314};315316// Run a terminal command in parallel on the compute servers of the given students.317// This does not throw an exception on error; instead, some entries in the output318// will have nonzero exit_code.319runTerminalCommand = async ({320unit,321student_ids,322setOutputs,323...terminalOptions324}) => {325let outputs: {326stdout?: string;327stderr?: string;328exit_code?: number;329student_id: string;330total_time: number;331}[] = [];332const timeout = terminalOptions.timeout;333const start = Date.now();334const task = async (student_id) => {335let result;336try {337result = {338...(await this.runTerminalCommandOneStudent({339unit,340student_id,341...terminalOptions,342err_on_exit: false,343})),344student_id,345total_time: (Date.now() - start) / 1000,346};347} catch (err) {348result = {349student_id,350stdout: "",351stderr: `${err}`,352exit_code: -1,353total_time: (Date.now() - start) / 1000,354timeout,355};356}357outputs = [...outputs, result];358setOutputs(outputs);359};360await awaitMap(student_ids, MAX_PARALLEL_TASKS, task);361return outputs;362};363364setComputeServerAssociations = async ({365src_path,366target_project_id,367target_path,368student_id,369unit_id,370}: {371src_path: string;372target_project_id: string;373target_path: string;374student_id: string;375unit_id: string;376}) => {377const { unit } = this.getUnit(unit_id);378const compute_server_id = this.getComputeServerId({ unit, student_id });379if (!compute_server_id) {380// If no compute server is configured for this student and unit,381// then nothing to do.382return;383}384385// Figure out which subdirectories in the src_path of the course project386// are on a compute server, and set them to be on THE compute server for387// this student/unit.388const store = this.getStore();389if (store == null) {390return;391}392const course_project_id = store.get("course_project_id");393394let studentAssociations: null | ComputeServerManager = null;395// project_client.computeServers can only be used for tabs396// for a project that is actually open in the client, so397// we use it for the instructor project, but not the student398// project, which may not be opened.399const courseAssociations =400webapp_client.project_client.computeServers(course_project_id);401402try {403studentAssociations = computeServerManager({404project_id: target_project_id,405});406407const ids = await courseAssociations.getServerIdForSubtree(src_path);408for (const source in ids) {409if (ids[source]) {410const tail = source.slice(src_path.length + 1);411const path = join(target_path, tail);412await studentAssociations.waitUntilReady();413// path is on a compute server.414studentAssociations.connectComputeServerToPath({415id: compute_server_id,416path,417});418}419}420} finally {421studentAssociations?.close();422}423};424}425426427