Skip to content

Commit 265404b

Browse files
committed
judge: clean up cached files used by multi-pass cases
1 parent c25c1d5 commit 265404b

5 files changed

Lines changed: 105 additions & 74 deletions

File tree

packages/hydrojudge/src/checkers.ts

Lines changed: 83 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@ export interface CheckConfig {
1616
env?: Record<string, string>;
1717
}
1818

19+
export interface NextPass extends AsyncDisposable {
20+
input: CopyInFile;
21+
state?: Record<string, CopyInFile>;
22+
}
23+
1924
type Checker = (config: CheckConfig) => Promise<{
2025
status: number;
2126
score: number;
2227
message: string | { message: string, params?: any[] };
23-
nextPass?: { input: CopyInFile, state?: Record<string, CopyInFile> };
28+
nextPass?: NextPass;
2429
}>;
2530

2631
function parseDiffMsg(msg: string) {
@@ -237,7 +242,7 @@ const checkers: Record<string, Checker> = new Proxy({
237242
},
238243

239244
async testlib(config) {
240-
const { stderr, status, code, fileIds } = await runQueued(`${config.execute} /w/in /w/user_out /w/answer`, {
245+
const res = await runQueued(`${config.execute} /w/in /w/user_out /w/answer`, {
241246
copyIn: {
242247
in: config.input,
243248
user_out: config.user_stdout,
@@ -248,41 +253,52 @@ const checkers: Record<string, Checker> = new Proxy({
248253
env: config.env,
249254
copyOutCached: ['nextpass.in?', 'state.txt?'],
250255
});
251-
if ([STATUS.STATUS_SYSTEM_ERROR, STATUS.STATUS_TIME_LIMIT_EXCEEDED, STATUS.STATUS_MEMORY_LIMIT_EXCEEDED].includes(status)) {
252-
const message = {
253-
[STATUS.STATUS_SYSTEM_ERROR]: stderr,
254-
[STATUS.STATUS_TIME_LIMIT_EXCEEDED]: 'Checker Time Limit Exceeded',
255-
[STATUS.STATUS_MEMORY_LIMIT_EXCEEDED]: 'Checker Memory Limit Exceeded',
256-
}[status];
257-
return {
258-
status: STATUS.STATUS_SYSTEM_ERROR,
259-
score: 0,
260-
message,
261-
};
256+
const cleanup = res[Symbol.asyncDispose];
257+
let cleanupTransferred = false;
258+
try {
259+
const {
260+
stderr, status, code, fileIds,
261+
} = res;
262+
if ([STATUS.STATUS_SYSTEM_ERROR, STATUS.STATUS_TIME_LIMIT_EXCEEDED, STATUS.STATUS_MEMORY_LIMIT_EXCEEDED].includes(status)) {
263+
const message = {
264+
[STATUS.STATUS_SYSTEM_ERROR]: stderr,
265+
[STATUS.STATUS_TIME_LIMIT_EXCEEDED]: 'Checker Time Limit Exceeded',
266+
[STATUS.STATUS_MEMORY_LIMIT_EXCEEDED]: 'Checker Memory Limit Exceeded',
267+
}[status];
268+
return {
269+
status: STATUS.STATUS_SYSTEM_ERROR,
270+
score: 0,
271+
message,
272+
};
273+
}
274+
if (status === STATUS.STATUS_RUNTIME_ERROR && !stderr?.trim()) {
275+
return {
276+
status: STATUS.STATUS_SYSTEM_ERROR,
277+
score: 0,
278+
message: `Checker exited with code ${code}`,
279+
};
280+
}
281+
const result = parse(stderr, config.score, config.detail);
282+
if (result.status === STATUS.STATUS_ACCEPTED && fileIds['nextpass.in']) {
283+
cleanupTransferred = true;
284+
return {
285+
...result,
286+
nextPass: {
287+
input: { fileId: fileIds['nextpass.in'] },
288+
state: fileIds['state.txt'] ? { 'state.txt': { fileId: fileIds['state.txt'] } } : undefined,
289+
[Symbol.asyncDispose]: cleanup,
290+
},
291+
};
292+
}
293+
return result;
294+
} finally {
295+
if (!cleanupTransferred) await cleanup();
262296
}
263-
if (status === STATUS.STATUS_RUNTIME_ERROR && !stderr?.trim()) {
264-
return {
265-
status: STATUS.STATUS_SYSTEM_ERROR,
266-
score: 0,
267-
message: `Checker exited with code ${code}`,
268-
};
269-
}
270-
const result = parse(stderr, config.score, config.detail);
271-
if (result.status === STATUS.STATUS_ACCEPTED && fileIds['nextpass.in']) {
272-
return {
273-
...result,
274-
nextPass: {
275-
input: { fileId: fileIds['nextpass.in'] },
276-
state: fileIds['state.txt'] ? { 'state.txt': { fileId: fileIds['state.txt'] } } : undefined,
277-
},
278-
};
279-
}
280-
return result;
281297
},
282298

283299
// https://www.kattis.com/problem-package-format/spec/2023-07-draft.html#output-validator
284300
async kattis(config) {
285-
const { files, fileIds, code } = await runQueued(`${config.execute} input answer_file feedback_dir`, {
301+
const res = await runQueued(`${config.execute} input answer_file feedback_dir`, {
286302
copyIn: {
287303
input: config.input,
288304
answer_file: config.output,
@@ -301,38 +317,46 @@ const checkers: Record<string, Checker> = new Proxy({
301317
'feedback_dir/state.txt?',
302318
],
303319
});
320+
const cleanup = res[Symbol.asyncDispose];
321+
let cleanupTransferred = false;
322+
try {
323+
const { files, fileIds, code } = res;
324+
const status = code === 42
325+
? STATUS.STATUS_ACCEPTED
326+
: code === 43
327+
? STATUS.STATUS_WRONG_ANSWER
328+
: STATUS.STATUS_SYSTEM_ERROR;
304329

305-
const status = code === 42
306-
? STATUS.STATUS_ACCEPTED
307-
: code === 43
308-
? STATUS.STATUS_WRONG_ANSWER
309-
: STATUS.STATUS_SYSTEM_ERROR;
330+
const score = status === STATUS.STATUS_ACCEPTED
331+
? config.score
332+
: +files['feedback_dir/score.txt'] || 0;
310333

311-
const score = status === STATUS.STATUS_ACCEPTED
312-
? config.score
313-
: +files['feedback_dir/score.txt'] || 0;
334+
const message = status === STATUS.STATUS_SYSTEM_ERROR
335+
? files['feedback_dir/judgeerror.txt'] || `Checker exited with code ${code}`
336+
: config.detail === 'full'
337+
? files['feedback_dir/teammessage.txt'] || files['feedback_dir/judgemessage.txt'] || ''
338+
: '';
314339

315-
const message = status === STATUS.STATUS_SYSTEM_ERROR
316-
? files['feedback_dir/judgeerror.txt'] || `Checker exited with code ${code}`
317-
: config.detail === 'full'
318-
? files['feedback_dir/teammessage.txt'] || files['feedback_dir/judgemessage.txt'] || ''
319-
: '';
340+
if (status === STATUS.STATUS_ACCEPTED && fileIds['feedback_dir/nextpass.in']) {
341+
cleanupTransferred = true;
342+
return {
343+
status,
344+
score,
345+
message,
346+
nextPass: {
347+
input: { fileId: fileIds['feedback_dir/nextpass.in'] },
348+
state: fileIds['feedback_dir/state.txt']
349+
? { 'feedback_dir/state.txt': { fileId: fileIds['feedback_dir/state.txt'] } }
350+
: undefined,
351+
[Symbol.asyncDispose]: cleanup,
352+
},
353+
};
354+
}
320355

321-
if (status === STATUS.STATUS_ACCEPTED && fileIds['feedback_dir/nextpass.in']) {
322-
return {
323-
status,
324-
score,
325-
message,
326-
nextPass: {
327-
input: { fileId: fileIds['feedback_dir/nextpass.in'] },
328-
state: fileIds['feedback_dir/state.txt']
329-
? { 'feedback_dir/state.txt': { fileId: fileIds['feedback_dir/state.txt'] } }
330-
: undefined,
331-
},
332-
};
356+
return { status, score, message };
357+
} finally {
358+
if (!cleanupTransferred) await cleanup();
333359
}
334-
335-
return { status, score, message };
336360
},
337361
}, {
338362
get(self, key) {

packages/hydrojudge/src/judge/communication.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function judgeCase(c: NormalizedCase) {
3939
});
4040
}
4141
execute[0].execute += managerArgs;
42-
const res = await runPiped(execute, pipeMapping, undefined, `judgeCase[${c.id}]<${ctx.rid}>`);
42+
await using res = await runPiped(execute, pipeMapping, undefined, `judgeCase[${c.id}]<${ctx.rid}>`);
4343
const resManager = res[0];
4444
let time = 0;
4545
let memory = 0;

packages/hydrojudge/src/judge/default.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NormalizedCase, STATUS } from '@hydrooj/common';
2-
import checkers from '../checkers';
2+
import checkers, { type NextPass } from '../checkers';
33
import { runFlow } from '../flow';
44
import { runQueued } from '../sandbox';
55
import signals from '../signals';
@@ -31,7 +31,7 @@ function judgeCase(c: NormalizedCase) {
3131
let { status } = res;
3232
let message: any = '';
3333
let score = 0;
34-
let nextPass: any;
34+
let nextPass: NextPass | undefined;
3535
if (status === STATUS.STATUS_ACCEPTED) {
3636
if (time > c.time) {
3737
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
@@ -69,9 +69,10 @@ function judgeCase(c: NormalizedCase) {
6969
if (mp.i && typeof message === 'string') message = `${message} [Pass ${mp.i}]`;
7070
}
7171
if (nextPass) {
72+
await using ownedNextPass = nextPass;
7273
if (mp.i < ctx.config.multi_pass) {
73-
mp.input = nextPass.input;
74-
mp.state = nextPass.state ?? undefined;
74+
mp.input = ownedNextPass.input;
75+
mp.state = ownedNextPass.state ?? undefined;
7576
mp.i++;
7677
return await runner(ctx, ctxSubtask, runner);
7778
}

packages/hydrojudge/src/judge/interactive.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ function judgeCase(c: NormalizedCase) {
1111
const { address_space_limit, process_limit } = ctx.session.getLang(ctx.lang);
1212
if (ctx.config.multi_pass && !mp.i) mp.i = 1;
1313

14-
const [{
15-
code, signalled, time, memory,
16-
}, resInteractor] = await runPiped([
14+
await using results = await runPiped([
1715
{
1816
execute: ctx.executeUser.execute,
1917
copyIn: { ...ctx.executeUser.copyIn, ...mp.state },
@@ -44,6 +42,9 @@ function judgeCase(c: NormalizedCase) {
4442
{ in: { index: 0, fd: 1 }, out: { index: 1, fd: 0 }, name: 'userToInteractor' },
4543
{ in: { index: 1, fd: 1 }, out: { index: 0, fd: 0 }, name: 'interactorToUser' },
4644
], undefined, `judgeCase[${c.id}]${mp.i ? `[pass=${mp.i}]` : ''}<${ctx.rid}>`);
45+
const [{
46+
code, signalled, time, memory,
47+
}, resInteractor] = results;
4748
// TODO handle tout (maybe pass to checker?)
4849
let status: number;
4950
let score = 0;

packages/hydrojudge/src/sandbox.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ interface SandboxAdaptedResult {
6262
error?: string;
6363
}
6464

65+
type DisposableSandboxResults = SandboxAdaptedResult[] & AsyncDisposable;
66+
6567
function checkStringArray(args: ParseEntry[]): args is string[] {
6668
return args.every((arg: ParseEntry) => typeof arg === 'string');
6769
}
@@ -147,9 +149,13 @@ function adaptResult(result: SandboxResult, params: Parameter): SandboxAdaptedRe
147149
return ret;
148150
}
149151

152+
export async function del(fileId: string) {
153+
await client.deleteFile(fileId);
154+
}
155+
150156
export async function runPiped(
151157
execute: Parameter[], pipeMapping: Pick<PipeMap, 'in' | 'out' | 'name'>[], params: Parameter = {}, trace: string = '',
152-
): Promise<SandboxAdaptedResult[]> {
158+
): Promise<DisposableSandboxResults> {
153159
let res: SandboxResult[];
154160
const size = parseMemoryMB(getConfig('stdio_size'));
155161
try {
@@ -179,11 +185,10 @@ export async function runPiped(
179185
console.error(e);
180186
throw new SystemError('Sandbox Error', [e]);
181187
}
182-
return res.map((r) => adaptResult(r, params)) as SandboxAdaptedResult[];
183-
}
184-
185-
export async function del(fileId: string) {
186-
await client.deleteFile(fileId);
188+
const result = res.map((r) => adaptResult(r, params)) as DisposableSandboxResults;
189+
const fileIds = new Set(result.flatMap((item) => Object.values(item.fileIds || {})));
190+
(result as any)[Symbol.asyncDispose] = () => Promise.allSettled([...fileIds].map(del));
191+
return result;
187192
}
188193

189194
export async function get(fileId: string, dest?: string) {
@@ -210,7 +215,7 @@ export function runQueued(
210215
return queue.add(async () => {
211216
const res = await runPiped(execute, pipeMapping, params, trace);
212217
const ret = single ? res[0] : res;
213-
(ret as any)[Symbol.asyncDispose] = () => Promise.allSettled(res.flatMap((t) => Object.values(t.fileIds || {}).map(del)));
218+
(ret as any)[Symbol.asyncDispose] = res[Symbol.asyncDispose];
214219
return ret;
215220
}, { priority });
216221
}

0 commit comments

Comments
 (0)