judge: let sandbox client manages cache clean up & fix cache clean up for parallel compile tasks (#955)

This commit is contained in:
Yang Gao authored and GitHub committed 2025-03-14 15:17:52 +00:00
1 parent ebc2eaecba
commit 875421dd5b
8 files changed
+41 -30

No files matched your search

+3 -3
View File
@@ -2,7 +2,7 @@ import { LangConfig, STATUS } from '@hydrooj/common';
import { CompileError } from './error'; import { CompileError } from './error';
import { Execute } from './interface'; import { Execute } from './interface';
import { import {
CopyIn, CopyInFile, del, runQueued, CopyIn, CopyInFile, runQueued,
} from './sandbox'; } from './sandbox';
import { compilerText } from './utils'; import { compilerText } from './utils';
@@ -14,7 +14,7 @@ export default async function compile(
const command = copyIn['compile.sh'] ? '/bin/bash compile.sh' : lang.compile; const command = copyIn['compile.sh'] ? '/bin/bash compile.sh' : lang.compile;
if (command) { if (command) {
const { const {
status, stdout, stderr, fileIds, status, stdout, stderr, fileIds, [Symbol.asyncDispose]: cleanup,
} = await runQueued( } = await runQueued(
command, command,
{ {
@@ -36,7 +36,7 @@ export default async function compile(
return { return {
execute, execute,
copyIn: { ...copyIn, [target]: { fileId: fileIds[target] } }, copyIn: { ...copyIn, [target]: { fileId: fileIds[target] } },
clean: () => del(fileIds[target]), clean: async () => await cleanup(),
_cacheable: target, _cacheable: target,
}; };
} }
+2 -3
View File
@@ -1,14 +1,14 @@
import { NormalizedCase, STATUS } from '@hydrooj/common'; import { NormalizedCase, STATUS } from '@hydrooj/common';
import checkers from '../checkers'; import checkers from '../checkers';
import { runFlow } from '../flow'; import { runFlow } from '../flow';
import { del, runQueued } from '../sandbox'; import { runQueued } from '../sandbox';
import signals from '../signals'; import signals from '../signals';
import { Context, ContextSubTask } from './interface'; import { Context, ContextSubTask } from './interface';
function judgeCase(c: NormalizedCase) { function judgeCase(c: NormalizedCase) {
return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => { return async (ctx: Context, ctxSubtask: ContextSubTask, runner?: Function) => {
const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang); const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang);
const res = await runQueued( await using res = await runQueued(
ctx.execute.execute, ctx.execute.execute,
{ {
stdin: { src: c.input }, stdin: { src: c.input },
@@ -57,7 +57,6 @@ function judgeCase(c: NormalizedCase) {
if (code < 32 && signalled) message = signals[code]; if (code < 32 && signalled) message = signals[code];
else message = { message: 'Your program returned {0}.', params: [code] }; else message = { message: 'Your program returned {0}.', params: [code] };
} }
await Promise.allSettled(Object.values(res.fileIds).map((id) => del(id)));
if (runner && ctx.rerun && c.time <= 5000 && status === STATUS.STATUS_TIME_LIMIT_EXCEEDED) { if (runner && ctx.rerun && c.time <= 5000 && status === STATUS.STATUS_TIME_LIMIT_EXCEEDED) {
ctx.rerun--; ctx.rerun--;
return await runner(ctx, ctxSubtask); return await runner(ctx, ctxSubtask);
+4 -6
View File
@@ -27,7 +27,7 @@ export const judge = async (ctx: JudgeTask) => {
let totalStatus = 0; let totalStatus = 0;
async function runGenerator(i: number) { async function runGenerator(i: number) {
const res = await runQueued( await using res = await runQueued(
`${executeGenerator.execute} ${i}`, `${executeGenerator.execute} ${i}`,
{ {
stdin: { content: ctx.input || '' }, stdin: { content: ctx.input || '' },
@@ -41,9 +41,8 @@ export const judge = async (ctx: JudgeTask) => {
1, 1,
); );
const tmp = path.join(tmpdir(), `${ctx.request.rid}.${i}.in`); const tmp = path.join(tmpdir(), `${ctx.request.rid}.${i}.in`);
ctx.clean.push(() => { ctx.pushClean(() => {
if (fs.existsSync(tmp)) fs.removeSync(tmp); if (fs.existsSync(tmp)) fs.removeSync(tmp);
return res.fileIds['stdout'] ? client.deleteFile(res.fileIds['stdout']) : Promise.resolve();
}); });
const { const {
code, signalled, time, memory, fileIds, stderr, code, signalled, time, memory, fileIds, stderr,
@@ -80,7 +79,7 @@ export const judge = async (ctx: JudgeTask) => {
return status === STATUS.STATUS_ACCEPTED ? tmp : null; return status === STATUS.STATUS_ACCEPTED ? tmp : null;
} }
async function runStd(i: number, stdin: CopyInFile) { async function runStd(i: number, stdin: CopyInFile) {
const res = await runQueued( await using res = await runQueued(
`${executeStd.execute} ${i}`, `${executeStd.execute} ${i}`,
{ {
stdin, stdin,
@@ -94,9 +93,8 @@ export const judge = async (ctx: JudgeTask) => {
1, 1,
); );
const tmp = path.join(tmpdir(), `${ctx.request.rid}.${i}.out`); const tmp = path.join(tmpdir(), `${ctx.request.rid}.${i}.out`);
ctx.clean.push(() => { ctx.pushClean(() => {
if (fs.existsSync(tmp)) fs.removeSync(tmp); if (fs.existsSync(tmp)) fs.removeSync(tmp);
return res.fileIds['stdout'] ? client.deleteFile(res.fileIds['stdout']) : Promise.resolve();
}); });
const { const {
code, signalled, time, memory, fileIds, stderr, code, signalled, time, memory, fileIds, stderr,
+7 -6
View File
@@ -2,7 +2,7 @@
import { STATUS } from '@hydrooj/common'; import { STATUS } from '@hydrooj/common';
import { fs, parseMemoryMB, parseTimeMS } from '@hydrooj/utils'; import { fs, parseMemoryMB, parseTimeMS } from '@hydrooj/utils';
import checkers from '../checkers'; import checkers from '../checkers';
import { del, runQueued } from '../sandbox'; import { runQueued } from '../sandbox';
import signals from '../signals'; import signals from '../signals';
import { Context } from './interface'; import { Context } from './interface';
@@ -12,9 +12,12 @@ export async function judge(ctx: Context) {
ctx.compile(ctx.lang, ctx.code), ctx.compile(ctx.lang, ctx.code),
ctx.compileLocalFile('checker', ctx.config.checker, ctx.config.checker_type), ctx.compileLocalFile('checker', ctx.config.checker, ctx.config.checker_type),
ctx.compileLocalFile('validator', ctx.config.validator), ctx.compileLocalFile('validator', ctx.config.validator),
ctx.session.fetchFile(null, { [ctx.files.hack]: '' }), (async () => {
const f = await ctx.session.fetchFile(null, { [ctx.files.hack]: '' });
ctx.pushClean(() => fs.unlink(f));
return f;
})(),
]); ]);
ctx.clean.push(() => fs.unlink(input));
ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 });
const validateResult = await runQueued( const validateResult = await runQueued(
validator.execute, validator.execute,
@@ -31,7 +34,7 @@ export async function judge(ctx: Context) {
return ctx.end({ status: STATUS.STATUS_FORMAT_ERROR, message }); return ctx.end({ status: STATUS.STATUS_FORMAT_ERROR, message });
} }
const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang); const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang);
const res = await runQueued( await using res = await runQueued(
execute.execute, execute.execute,
{ {
stdin: { src: input }, stdin: { src: input },
@@ -72,8 +75,6 @@ export async function judge(ctx: Context) {
if (code < 32 && signalled) message = signals[code]; if (code < 32 && signalled) message = signals[code];
else message = { message: 'Your program returned {0}.', params: [code] }; else message = { message: 'Your program returned {0}.', params: [code] };
} }
await Promise.allSettled(Object.values(res.fileIds).map((id) => del(id)));
if (message) ctx.next({ message }); if (message) ctx.next({ message });
return ctx.end({ return ctx.end({
@@ -2,7 +2,7 @@ import { NormalizedCase, STATUS } from '@hydrooj/common';
import { fs } from '@hydrooj/utils'; import { fs } from '@hydrooj/utils';
import checkers from '../checkers'; import checkers from '../checkers';
import { runFlow } from '../flow'; import { runFlow } from '../flow';
import { del, runQueued } from '../sandbox'; import { runQueued } from '../sandbox';
import { Context } from './interface'; import { Context } from './interface';
function judgeCase(c: NormalizedCase) { function judgeCase(c: NormalizedCase) {
@@ -15,9 +15,8 @@ function judgeCase(c: NormalizedCase) {
let status = STATUS.STATUS_ACCEPTED; let status = STATUS.STATUS_ACCEPTED;
let message: any = ''; let message: any = '';
let score = 0; let score = 0;
const fileIds = [];
if (ctx.config.subType === 'multi') { if (ctx.config.subType === 'multi') {
const res = await runQueued( await using res = await runQueued(
'/usr/bin/unzip foo.zip', '/usr/bin/unzip foo.zip',
{ {
stdin: null, stdin: null,
@@ -36,7 +35,6 @@ function judgeCase(c: NormalizedCase) {
status = STATUS.STATUS_WRONG_ANSWER; status = STATUS.STATUS_WRONG_ANSWER;
} }
file = { fileId: res.fileIds[name] }; file = { fileId: res.fileIds[name] };
fileIds.push(...Object.values(res.fileIds));
} }
if (status === STATUS.STATUS_ACCEPTED) { if (status === STATUS.STATUS_ACCEPTED) {
({ status, score, message } = await checkers[ctx.config.checker_type]({ ({ status, score, message } = await checkers[ctx.config.checker_type]({
@@ -52,7 +50,6 @@ function judgeCase(c: NormalizedCase) {
env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() }, env: { ...ctx.env, HYDRO_TESTCASE: c.id.toString() },
})); }));
} }
await Promise.allSettled(fileIds.map(del));
return { return {
id: c.id, id: c.id,
status, status,
+7 -4
View File
@@ -195,8 +195,9 @@ const queue = new PQueue({ concurrency: getConfig('concurrency') || getConfig('p
export function runQueued( export function runQueued(
execute: Parameter[], pipeMapping: Pick<PipeMap, 'in' | 'out' | 'name'>[], execute: Parameter[], pipeMapping: Pick<PipeMap, 'in' | 'out' | 'name'>[],
params: Parameter, trace?: string, priority?: number, params: Parameter, trace?: string, priority?: number,
): Promise<SandboxAdaptedResult[]>; ): Promise<SandboxAdaptedResult[] & AsyncDisposable>;
export function runQueued(execute: string, params: Parameter, trace?: string, priority?: number): Promise<SandboxAdaptedResult>; export function runQueued(execute: string, params: Parameter, trace?: string, priority?: number
): Promise<SandboxAdaptedResult & AsyncDisposable>;
export function runQueued( export function runQueued(
arg0: string | Parameter[], arg1: Pick<PipeMap, 'in' | 'out' | 'name'>[] | Parameter, arg0: string | Parameter[], arg1: Pick<PipeMap, 'in' | 'out' | 'name'>[] | Parameter,
arg2?: string | Parameter, arg3?: string | number, arg4?: number, arg2?: string | Parameter, arg3?: string | number, arg4?: number,
@@ -207,7 +208,9 @@ export function runQueued(
: [arg0, arg1, arg2 || {}, arg3 || '', arg4 || 0]; : [arg0, arg1, arg2 || {}, arg3 || '', arg4 || 0];
return queue.add(async () => { return queue.add(async () => {
const res = await runPiped(execute, pipeMapping, params, trace); const res = await runPiped(execute, pipeMapping, params, trace);
return single ? res[0] : res; const ret = single ? res[0] : res;
(ret as any)[Symbol.asyncDispose] = () => Promise.allSettled(res.flatMap((t) => Object.values(t.fileIds || {}).map(del)));
return ret;
}, { priority }); }, { priority });
} }
@@ -228,7 +231,7 @@ export async function versionCheck(reportWarn: (str: string) => void, reportErro
} }
const { osinfo } = await sysinfo.get(); const { osinfo } = await sysinfo.get();
if (sandboxCgroup === 2) { if (sandboxCgroup === 2) {
const kernelVersion = osinfo.kernel.split('-')[0]; const kernelVersion = osinfo.kernel.match(/^\d+\.\d+\.\d+/)[0];
if (!gte(kernelVersion, '5.19.0') || !gte(sandboxVersion, '1.6.10')) { if (!gte(kernelVersion, '5.19.0') || !gte(sandboxVersion, '1.6.10')) {
reportWarn('You are using cgroup v2 without kernel 5.19+. This could result in inaccurate memory usage measurements.'); reportWarn('You are using cgroup v2 without kernel 5.19+. This could result in inaccurate memory usage measurements.');
} }
@@ -7,6 +7,7 @@ export interface SandboxVersion {
pipeProxy?: boolean; pipeProxy?: boolean;
addressSpaceLimit?: boolean; addressSpaceLimit?: boolean;
stream?: boolean; stream?: boolean;
procPeak?: boolean;
} }
export interface LocalFile { export interface LocalFile {
@@ -157,6 +158,8 @@ export interface SandboxResult {
memory: number; memory: number;
/** wall clock time in ns */ /** wall clock time in ns */
runTime: number; runTime: number;
/** peak count of processes (thread) during execution */
procPeak?: number;
/** copyOut file name to content (UTF-8 encoded and invalid character replaced) */ /** copyOut file name to content (UTF-8 encoded and invalid character replaced) */
files?: Record<string, string>; files?: Record<string, string>;
/** copyOutCached file name to fileId */ /** copyOutCached file name to fileId */
+13 -3
View File
@@ -31,6 +31,7 @@ export class JudgeTask {
lang: string; lang: string;
code: CopyInFile; code: CopyInFile;
input?: string; input?: string;
finished: boolean = false;
clean: (() => Promise<any>)[] = []; clean: (() => Promise<any>)[] = [];
data: FileInfo[]; data: FileInfo[];
folder: string; folder: string;
@@ -93,6 +94,7 @@ export class JudgeTask {
}); });
} }
} finally { } finally {
this.finished = true;
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
for (const clean of this.clean) await clean()?.catch(() => null); for (const clean of this.clean) await clean()?.catch(() => null);
} }
@@ -174,12 +176,17 @@ export class JudgeTask {
await judge[type].judge(this); await judge[type].judge(this);
} }
async pushClean(f: () => any | Promise<any>) {
if (this.finished) await f().catch(() => null);
else this.clean.push(f);
}
async compile(lang: string, code: CopyInFile) { async compile(lang: string, code: CopyInFile) {
const copyIn = Object.fromEntries( const copyIn = Object.fromEntries(
(this.config.user_extra_files || []).map((i) => [basename(i), { src: i }]), (this.config.user_extra_files || []).map((i) => [basename(i), { src: i }]),
) as CopyIn; ) as CopyIn;
const result = await compile(this.session.getLang(lang), code, copyIn, this.next); const result = await compile(this.session.getLang(lang), code, copyIn, this.next);
this.clean.push(result.clean); await this.pushClean(result.clean);
return result; return result;
} }
@@ -218,8 +225,10 @@ export class JudgeTask {
if (!lang) throw new FormatError(`Unknown ${type} language.`); if (!lang) throw new FormatError(`Unknown ${type} language.`);
// TODO cache compiled binary // TODO cache compiled binary
const result = await compile(lang, { src: file }, copyIn); const result = await compile(lang, { src: file }, copyIn);
this.clean.push(result.clean); if (!result._cacheable) {
if (!result._cacheable) return result; await this.pushClean(result.clean);
return result;
}
await Lock.acquire(this.folder); await Lock.acquire(this.folder);
try { try {
const loc = join(this.folder, `_${type}.cache`); const loc = join(this.folder, `_${type}.cache`);
@@ -235,6 +244,7 @@ export class JudgeTask {
const currEtag = await fs.readFile(join(this.folder, 'etags'), 'utf-8'); const currEtag = await fs.readFile(join(this.folder, 'etags'), 'utf-8');
await fs.writeFile(join(this.folder, 'etags'), JSON.stringify({ ...JSON.parse(currEtag), '*cache': this.compileCache })); await fs.writeFile(join(this.folder, 'etags'), JSON.stringify({ ...JSON.parse(currEtag), '*cache': this.compileCache }));
} finally { } finally {
await this.pushClean(result.clean);
Lock.release(this.folder); Lock.release(this.folder);
} }
return result; return result;