judge: add tracing detail for judge case and fetch file

This commit is contained in:
undefined committed 2025-12-30 07:18:39 +08:00
1 parent 0002801058
commit 907de9c37f
6 files changed
+20 -10

No files matched your search

+2 -2
View File
@@ -10,7 +10,7 @@ const defaultEnv = `\
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/w
# modify to your python version installed
PYTHONPATH=/lib/python3.12/site-packages
PYTHONPATH=/lib/python3.13/site-packages
`;
export const JudgeSettings = Schema.object({
@@ -28,7 +28,7 @@ export const JudgeSettings = Schema.object({
singleTaskParallelism: Schema.number().default(2).min(1).step(1),
rerun: Schema.number().description('Re-Run testcase if time-limit-exceeded (max per submission)').default(0).min(0).step(1),
rate: Schema.number().default(1),
env: Schema.string().default(defaultEnv),
env: Schema.string().default(defaultEnv).role('textarea'),
host: Schema.any(),
secret: Schema.string().description('Judge Token Secret').default(randomstring(32)),
disable: Schema.boolean().description('Disable builtin judge').default(false),
+6 -1
View File
@@ -45,7 +45,12 @@ function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task['
time: 0,
memory: 0,
message: '',
} : await runner(ctx, ctxSubtask, runner);
} : await (async () => {
using span = ctx.startChildSpan('judge.case', { id: subtask.cases[cid].id, subtaskId: subtask.id });
const r = await runner(ctx, ctxSubtask, runner);
span.setAttributes({ status: r?.status, time: r?.time, memory: r?.memory });
return r;
})();
if (res?.status !== STATUS.STATUS_CANCELED) {
ctxSubtask.score = Score[ctxSubtask.subtask.type](ctxSubtask.score, res.score);
ctxSubtask.status = Math.max(ctxSubtask.status, res.status);
+4 -1
View File
@@ -1,4 +1,5 @@
import path from 'path';
import { SpanStatusCode } from '@opentelemetry/api';
import PQueue from 'p-queue';
import superagent from 'superagent';
import WebSocket from 'ws';
@@ -49,7 +50,7 @@ export default class Hydro implements Session {
setInterval(() => { this.get(''); }, 30000000); // Cookie refresh only
}
async fetchFile<T extends string | null>(namespace: T, files: Record<string, string>): Promise<T extends null ? string : null> {
async fetchFile<T extends string | null>(namespace: T, files: Record<string, string>, ctx: JudgeTask): Promise<T extends null ? string : null> {
if (!namespace) { // record-related resource (code)
const name = Object.keys(files)[0].split('#')[0];
const res = await this.post('judge/files', { id: name });
@@ -71,9 +72,11 @@ export default class Hydro implements Session {
});
for (const name in res.body.links) {
queue.add(async () => {
using span = ctx.startChildSpan('judge.fetchFile', { name });
if (name.includes('/')) await fs.ensureDir(path.dirname(files[name]));
const w = fs.createWriteStream(files[name]);
await pipeRequest(this.get(res.body.links[name]), w, 60000, name);
span.setStatus({ code: SpanStatusCode.OK });
});
}
await queue.onIdle();
+1 -1
View File
@@ -27,7 +27,7 @@ export { JudgeRequest } from '@hydrooj/common';
export interface Session {
getLang: (name: string, doThrow?: boolean) => LangConfig;
getReporter: (task: JudgeTask) => { next: NextFunction, end: NextFunction };
fetchFile: <T extends null | string>(namespace: T, files: Record<string, string>) => Promise<T extends null ? string : null>;
fetchFile: <T extends null | string>(namespace: T, files: Record<string, string>, ctx: JudgeTask) => Promise<T extends null ? string : null>;
postFile: (target: string, filename: string, file: string) => Promise<void>;
config: { detail: DetailType, host?: string, trusted?: boolean };
}
+1 -1
View File
@@ -12,7 +12,7 @@ export async function judge(ctx: Context) {
ctx.compileLocalFile('checker', ctx.config.checker, ctx.config.checker_type),
ctx.compileLocalFile('validator', ctx.config.validator),
(async () => {
const f = await ctx.session.fetchFile(null, { [ctx.files.hack]: '' });
const f = await ctx.session.fetchFile(null, { [ctx.files.hack]: '' }, ctx);
ctx.pushClean(() => fs.unlink(f));
return f;
})(),
+6 -4
View File
@@ -51,7 +51,7 @@ export class JudgeTask {
logger.debug('%o', request);
}
private startChildSpan(name: string, attributes?: Record<string, any>) {
startChildSpan(name: string, attributes?: Record<string, any>) {
const span = this.tracer.startSpan(name, { attributes }, this.mainContext);
span[Symbol.dispose] = () => span.end();
return span as typeof span & { [Symbol.dispose]: () => void };
@@ -142,13 +142,14 @@ export class JudgeTask {
const allFilesToRemove = Object.keys(etags).filter((name) => !allFiles.has(name) && fs.existsSync(join(filePath, name)));
await Promise.all(allFilesToRemove.map((name) => fs.remove(join(filePath, name))));
if (filenames.length) {
span.setAttribute('syncedFiles', filenames.length);
span.setAttribute('files', filenames);
logger.info(`Getting problem data: ${this.session?.config.host || 'local'}/${source}`);
this.next({ message: 'Syncing testdata, please wait...' });
this.mainContext = trace.setSpan(context.active(), span);
await this.session.fetchFile(source, Object.fromEntries(
files.filter((i) => filenames.includes(i.name))
.map((i) => [i.name, join(filePath, i.name)]),
));
), this);
this.compileCache = {};
}
if (allFilesToRemove.length || filenames.length) {
@@ -161,6 +162,7 @@ export class JudgeTask {
logger.warn('CacheOpen Fail: %s %o %o', source, files, e);
throw e;
} finally {
this.mainContext = trace.setSpan(context.active(), this.span);
Lock.release(filePath);
}
}
@@ -168,7 +170,7 @@ export class JudgeTask {
async doSubmission() {
this.folder = await this.cacheOpen(this.source, this.data);
if (this.files?.code) {
const target = await this.session.fetchFile(null, { [this.files.code]: '' });
const target = await this.session.fetchFile(null, { [this.files.code]: '' }, this);
this.code = { src: target };
this.clean.push(() => fs.remove(target));
}