From 907de9c37f2da4744cd64d2d2064baca4338600f Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 30 Dec 2025 07:18:39 +0800 Subject: [PATCH] judge: add tracing detail for judge case and fetch file --- packages/hydrojudge/src/config.ts | 4 ++-- packages/hydrojudge/src/flow.ts | 7 ++++++- packages/hydrojudge/src/hosts/hydro.ts | 5 ++++- packages/hydrojudge/src/interface.ts | 2 +- packages/hydrojudge/src/judge/hack.ts | 2 +- packages/hydrojudge/src/task.ts | 10 ++++++---- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/hydrojudge/src/config.ts b/packages/hydrojudge/src/config.ts index 7a24b8e4f..51b2b9f27 100644 --- a/packages/hydrojudge/src/config.ts +++ b/packages/hydrojudge/src/config.ts @@ -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), diff --git a/packages/hydrojudge/src/flow.ts b/packages/hydrojudge/src/flow.ts index 9a4ac0d48..71a74e0b3 100644 --- a/packages/hydrojudge/src/flow.ts +++ b/packages/hydrojudge/src/flow.ts @@ -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); diff --git a/packages/hydrojudge/src/hosts/hydro.ts b/packages/hydrojudge/src/hosts/hydro.ts index c50295a96..8c728d100 100644 --- a/packages/hydrojudge/src/hosts/hydro.ts +++ b/packages/hydrojudge/src/hosts/hydro.ts @@ -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(namespace: T, files: Record): Promise { + async fetchFile(namespace: T, files: Record, ctx: JudgeTask): Promise { 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(); diff --git a/packages/hydrojudge/src/interface.ts b/packages/hydrojudge/src/interface.ts index e85bfd1bc..e9426a1c4 100644 --- a/packages/hydrojudge/src/interface.ts +++ b/packages/hydrojudge/src/interface.ts @@ -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: (namespace: T, files: Record) => Promise; + fetchFile: (namespace: T, files: Record, ctx: JudgeTask) => Promise; postFile: (target: string, filename: string, file: string) => Promise; config: { detail: DetailType, host?: string, trusted?: boolean }; } diff --git a/packages/hydrojudge/src/judge/hack.ts b/packages/hydrojudge/src/judge/hack.ts index 681c9edb4..79257ac63 100644 --- a/packages/hydrojudge/src/judge/hack.ts +++ b/packages/hydrojudge/src/judge/hack.ts @@ -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; })(), diff --git a/packages/hydrojudge/src/task.ts b/packages/hydrojudge/src/task.ts index aec0d46a1..8adc02388 100644 --- a/packages/hydrojudge/src/task.ts +++ b/packages/hydrojudge/src/task.ts @@ -51,7 +51,7 @@ export class JudgeTask { logger.debug('%o', request); } - private startChildSpan(name: string, attributes?: Record) { + startChildSpan(name: string, attributes?: Record) { 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)); }