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