mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
judge: let sandbox client manages cache clean up & fix cache clean up for parallel compile tasks (#955)
This commit is contained in:
1 parent
ebc2eaecba
commit
875421dd5b
8 files changed
+41
-30
No files matched your search
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user