refactor(judge): rewrite judge service to use database submissions

- Change return type from JudgeResult to Submission
- Add database operations to persist submission records
- Improve error handling and container cleanup
- Update compile and run functions to work with submissions
- Add proper status updates throughout the process
This commit is contained in:
cfngc4594 2025-04-11 09:52:56 +08:00
parent dc939085bb
commit d64f95b4e7

View File

@ -4,11 +4,11 @@ import fs from "fs";
import tar from "tar-stream"; import tar from "tar-stream";
import Docker from "dockerode"; import Docker from "dockerode";
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { v4 as uuid } from "uuid";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { Readable, Writable } from "stream"; import { Readable, Writable } from "stream";
import { ExitCode, EditorLanguage, JudgeResult } from "@/generated/client"; import { Status } from "@/generated/client";
import type { EditorLanguage, Submission } from "@/generated/client";
const isRemote = process.env.DOCKER_HOST_MODE === "remote"; const isRemote = process.env.DOCKER_HOST_MODE === "remote";
@ -25,11 +25,16 @@ const docker = isRemote
: new Docker({ socketPath: "/var/run/docker.sock" }); : new Docker({ socketPath: "/var/run/docker.sock" });
// Prepare Docker image environment // Prepare Docker image environment
async function prepareEnvironment(image: string, tag: string) { async function prepareEnvironment(image: string, tag: string): Promise<boolean> {
try {
const reference = `${image}:${tag}`; const reference = `${image}:${tag}`;
const filters = { reference: [reference] }; const filters = { reference: [reference] };
const images = await docker.listImages({ filters }); const images = await docker.listImages({ filters });
if (images.length === 0) await docker.pull(reference); return images.length !== 0;
} catch (error) {
console.error("Error checking Docker images:", error);
return false;
}
} }
// Create Docker container with keep-alive // Create Docker container with keep-alive
@ -64,15 +69,35 @@ function createTarStream(file: string, value: string) {
export async function judge( export async function judge(
language: EditorLanguage, language: EditorLanguage,
value: string, code: string,
problemId: string, problemId: string,
): Promise<JudgeResult> { ): Promise<Submission> {
const session = await auth(); const session = await auth();
if (!session) redirect("/sign-in"); if (!session?.user?.id) redirect("/sign-in");
const userId = session.user.id;
let container: Docker.Container | null = null; let container: Docker.Container | null = null;
let submission: Submission | null = null;
try { try {
const problem = await prisma.problem.findUnique({
where: { id: problemId },
});
if (!problem) {
submission = await prisma.submission.create({
data: {
language,
code,
status: Status.SE,
userId,
problemId,
message: "Problem not found",
},
});
return submission;
}
const config = await prisma.editorLanguageConfig.findUnique({ const config = await prisma.editorLanguageConfig.findUnique({
where: { language }, where: { language },
include: { include: {
@ -80,32 +105,18 @@ export async function judge(
}, },
}); });
if (!config || !config.dockerConfig) { if (!config?.dockerConfig) {
return { submission = await prisma.submission.create({
id: uuid(), data: {
output: "Configuration Error: Missing editor or docker configuration", language,
exitCode: ExitCode.SE, code,
executionTime: null, status: Status.SE,
memoryUsage: null, userId,
}; problemId,
} message: " Missing editor or docker configuration",
const problem = await prisma.problem.findUnique({
where: { id: problemId },
select: {
timeLimit: true,
memoryLimit: true,
}, },
}); });
return submission;
if (!problem) {
return {
id: uuid(),
output: "Problem not found.",
exitCode: ExitCode.SE,
executionTime: null,
memoryUsage: null,
};
} }
const { const {
@ -119,35 +130,78 @@ export async function judge(
const file = `${fileName}.${fileExtension}`; const file = `${fileName}.${fileExtension}`;
// Prepare the environment and create a container // Prepare the environment and create a container
await prepareEnvironment(image, tag); if (await prepareEnvironment(image, tag)) {
container = await createContainer(image, tag, workingDir, problem.memoryLimit); container = await createContainer(image, tag, workingDir, problem.memoryLimit);
} else {
submission = await prisma.submission.create({
data: {
language,
code,
status: Status.SE,
userId,
problemId,
message: "The docker environment is not ready",
},
});
return submission;
}
submission = await prisma.submission.create({
data: {
language,
code,
status: Status.PD,
userId,
problemId,
message: "",
},
});
// Upload code to the container // Upload code to the container
const tarStream = createTarStream(file, value); const tarStream = createTarStream(file, code);
await container.putArchive(tarStream, { path: workingDir }); await container.putArchive(tarStream, { path: workingDir });
// Compile the code // Compile the code
const compileResult = await compile(container, file, fileName, compileOutputLimit); const compileResult = await compile(container, file, fileName, compileOutputLimit, submission.id);
if (compileResult.exitCode === ExitCode.CE) { if (compileResult.status === Status.CE) {
return compileResult; return compileResult;
} }
// Run the code // Run the code
const runResult = await run(container, fileName, problem.timeLimit, runOutputLimit); const runResult = await run(container, fileName, problem.timeLimit, runOutputLimit, submission.id);
return runResult; return runResult;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return { if (submission) {
id: uuid(), const updatedSubmission = await prisma.submission.update({
output: "System Error", where: { id: submission.id },
exitCode: ExitCode.SE, data: {
executionTime: null, status: Status.SE,
memoryUsage: null, message: "System Error",
}; }
})
return updatedSubmission;
} else {
submission = await prisma.submission.create({
data: {
language,
code,
status: Status.PD,
userId,
problemId,
message: "",
},
})
return submission;
}
} finally { } finally {
if (container) { if (container) {
try {
await container.kill(); await container.kill();
await container.remove(); await container.remove();
} catch (error) {
console.error("Container cleanup failed:", error);
}
} }
} }
} }
@ -156,18 +210,19 @@ async function compile(
container: Docker.Container, container: Docker.Container,
file: string, file: string,
fileName: string, fileName: string,
maxOutput: number = 1 * 1024 * 1024 compileOutputLimit: number = 1 * 1024 * 1024,
): Promise<JudgeResult> { submissionId: string,
): Promise<Submission> {
const compileExec = await container.exec({ const compileExec = await container.exec({
Cmd: ["gcc", "-O2", file, "-o", fileName], Cmd: ["gcc", "-O2", file, "-o", fileName],
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
}); });
return new Promise<JudgeResult>((resolve, reject) => { return new Promise<Submission>((resolve, reject) => {
compileExec.start({}, (error, stream) => { compileExec.start({}, (error, stream) => {
if (error || !stream) { if (error || !stream) {
return reject({ output: "System Error", exitCode: ExitCode.SE }); return reject({ message: "System Error", Status: Status.SE });
} }
const stdoutChunks: string[] = []; const stdoutChunks: string[] = [];
@ -175,10 +230,10 @@ async function compile(
const stdoutStream = new Writable({ const stdoutStream = new Writable({
write(chunk, _encoding, callback) { write(chunk, _encoding, callback) {
let text = chunk.toString(); let text = chunk.toString();
if (stdoutLength + text.length > maxOutput) { if (stdoutLength + text.length > compileOutputLimit) {
text = text.substring(0, maxOutput - stdoutLength); text = text.substring(0, compileOutputLimit - stdoutLength);
stdoutChunks.push(text); stdoutChunks.push(text);
stdoutLength = maxOutput; stdoutLength = compileOutputLimit;
callback(); callback();
return; return;
} }
@ -193,10 +248,10 @@ async function compile(
const stderrStream = new Writable({ const stderrStream = new Writable({
write(chunk, _encoding, callback) { write(chunk, _encoding, callback) {
let text = chunk.toString(); let text = chunk.toString();
if (stderrLength + text.length > maxOutput) { if (stderrLength + text.length > compileOutputLimit) {
text = text.substring(0, maxOutput - stderrLength); text = text.substring(0, compileOutputLimit - stderrLength);
stderrChunks.push(text); stderrChunks.push(text);
stderrLength = maxOutput; stderrLength = compileOutputLimit;
callback(); callback();
return; return;
} }
@ -213,31 +268,31 @@ async function compile(
const stderr = stderrChunks.join(""); const stderr = stderrChunks.join("");
const exitCode = (await compileExec.inspect()).ExitCode; const exitCode = (await compileExec.inspect()).ExitCode;
let result: JudgeResult; let updatedSubmission: Submission;
if (exitCode !== 0 || stderr) { if (exitCode !== 0 || stderr) {
result = { updatedSubmission = await prisma.submission.update({
id: uuid(), where: { id: submissionId },
output: stderr || "Compilation Error", data: {
exitCode: ExitCode.CE, status: Status.CE,
executionTime: null, message: stderr || "Compilation Error",
memoryUsage: null, },
}; });
} else { } else {
result = { updatedSubmission = await prisma.submission.update({
id: uuid(), where: { id: submissionId },
output: stdout, data: {
exitCode: ExitCode.CS, status: Status.CS,
executionTime: null, message: stdout,
memoryUsage: null, },
}; });
} }
resolve(result); resolve(updatedSubmission);
}); });
stream.on("error", () => { stream.on("error", () => {
reject({ output: "System Error", exitCode: ExitCode.SE }); reject({ message: "System Error", Status: Status.SE });
}); });
}); });
}); });
@ -249,7 +304,8 @@ async function run(
fileName: string, fileName: string,
timeLimit: number = 1000, timeLimit: number = 1000,
maxOutput: number = 1 * 1024 * 1024, maxOutput: number = 1 * 1024 * 1024,
): Promise<JudgeResult> { submissionId: string,
): Promise<Submission> {
const runExec = await container.exec({ const runExec = await container.exec({
Cmd: [`./${fileName}`], Cmd: [`./${fileName}`],
AttachStdout: true, AttachStdout: true,
@ -257,7 +313,7 @@ async function run(
AttachStdin: true, AttachStdin: true,
}); });
return new Promise<JudgeResult>((resolve, reject) => { return new Promise<Submission>((resolve, reject) => {
const stdoutChunks: string[] = []; const stdoutChunks: string[] = [];
let stdoutLength = 0; let stdoutLength = 0;
const stdoutStream = new Writable({ const stdoutStream = new Writable({
@ -297,7 +353,7 @@ async function run(
// Start the exec stream // Start the exec stream
runExec.start({ hijack: true }, (error, stream) => { runExec.start({ hijack: true }, (error, stream) => {
if (error || !stream) { if (error || !stream) {
return reject({ output: "System Error", exitCode: ExitCode.SE }); return reject({ message: "System Error", status: Status.SE });
} }
stream.write("[2,7,11,15]\n9\n[3,2,4]\n6\n[3,3]\n6"); stream.write("[2,7,11,15]\n9\n[3,2,4]\n6\n[3,3]\n6");
@ -307,13 +363,14 @@ async function run(
// Timeout mechanism // Timeout mechanism
const timeoutId = setTimeout(async () => { const timeoutId = setTimeout(async () => {
resolve({ const updatedSubmission = await prisma.submission.update({
id: uuid(), where: { id: submissionId },
output: "Time Limit Exceeded", data: {
exitCode: ExitCode.TLE, status: Status.TLE,
executionTime: null, message: "Time Limit Exceeded",
memoryUsage: null, }
}); })
resolve(updatedSubmission);
}, timeLimit); }, timeLimit);
stream.on("end", async () => { stream.on("end", async () => {
@ -322,41 +379,40 @@ async function run(
const stderr = stderrChunks.join(""); const stderr = stderrChunks.join("");
const exitCode = (await runExec.inspect()).ExitCode; const exitCode = (await runExec.inspect()).ExitCode;
let result: JudgeResult; let updatedSubmission: Submission;
// Exit code 0 means successful execution // Exit code 0 means successful execution
if (exitCode === 0) { if (exitCode === 0) {
result = { updatedSubmission = await prisma.submission.update({
id: uuid(), where: { id: submissionId },
output: stdout, data: {
exitCode: ExitCode.AC, status: Status.AC,
executionTime: null, message: stdout,
memoryUsage: null,
};
} else if (exitCode === 137) {
result = {
id: uuid(),
output: stderr || "Memory Limit Exceeded",
exitCode: ExitCode.MLE,
executionTime: null,
memoryUsage: null,
};
} else {
result = {
id: uuid(),
output: stderr || "Runtime Error",
exitCode: ExitCode.RE,
executionTime: null,
memoryUsage: null,
};
} }
})
resolve(result); } else if (exitCode === 137) {
updatedSubmission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.MLE,
message: stderr || "Memory Limit Exceeded",
}
})
} else {
updatedSubmission = await prisma.submission.update({
where: { id: submissionId },
data: {
status: Status.RE,
message: stderr || "Runtime Error",
}
})
}
resolve(updatedSubmission);
}); });
stream.on("error", () => { stream.on("error", () => {
clearTimeout(timeoutId); // Clear timeout in case of error clearTimeout(timeoutId); // Clear timeout in case of error
reject({ output: "System Error", exitCode: ExitCode.SE }); reject({ message: "System Error", Status: Status.SE });
}); });
}); });
}); });