perf(判题): 判题机上同时跑的程序从最多 8 个压到 4 个,挤的时候不再把不超时的判成超时

原来四道闸各管各的(worker 判题 2、api 试运行 2、提交前编译检查 2、worker 里流程图
评分的试运行又 2),api 和 worker 两个进程互相看不见,峰值 8 个程序压在服务器两个核上;
判题机按墙钟判超时,挤到一定程度会冤枉学生。

judge/slots.ts 在 Redis 里按池数名额(每个站点各数各的):
- 后台池(正式判题、诊断重跑、流程图评分)上限 JUDGE_CONCURRENCY
- 交互池(运行例子、自己输入、生成测试点、编译检查)上限 TRIAL_CONCURRENCY
两池不互相借,正式判题排再长也挤不掉学生的「运行例子」。名额是带到期时间的租约,
进程被杀了到期自动作废;Redis 出错直接放行。正式判题的超时从拿到名额才开始算。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-16 14:47:24 +08:00
1 parent 64994b9499
commit 307b0725d0
4 files changed
+159 -41

No files matched your search

+19 -39
View File
@@ -15,6 +15,7 @@ import type { JudgeJobData } from "./job"
import { judgeConfigFor } from "./languages"
import { isAccepted, JudgeStatus, type JudgeStatusValue } from "./status"
import { parseProblemTemplate, shiftCompileLines } from "./template"
import { withJudgeSlot } from "./slots"
import { runSqlCase } from "./sql"
import {
nativeRuntimeError,
@@ -121,46 +122,22 @@ export async function postJudge(
* 判题机只有 /judge 一个入口,得带一组测试点:给一组空输入,跑的那一下限 1 秒。
* 判题机连不上、超时,一律当编得过 —— 那就和原来一样照常格式化。
*
* 这一下不进判题队列,所以自己排一道:同时最多 COMPILE_CHECK_CONCURRENCY 份在判题机上编,
* 其余等着。服务器只有两核,不排的话一个班 40 人一起交 C++ 能把判题机挤到每人 8 秒超时,
* 还拖慢正式判题。8 秒是连排队带编译的总预算,排不上也当编得过。
* 这一下不进判题队列,但和运行例子共用交互池的名额(judge/slots.ts):服务器只有两核,
* 不排的话一个班 40 人一起交 C++ 能把判题机挤到每人 8 秒超时,还拖慢正式判题。
* 8 秒是连排队带编译的总预算,排不上也当编得过。
*/
const COMPILE_CHECK_CONCURRENCY = 2
let compileChecks = 0
const compileCheckWaiting: (() => void)[] = []
export async function compileFails(language: string, source: string) {
const deadline = AbortSignal.timeout(8000)
if (compileChecks < COMPILE_CHECK_CONCURRENCY) {
compileChecks++
} else {
const admitted = await new Promise<boolean>((resolve) => {
const giveUp = () => {
compileCheckWaiting.splice(compileCheckWaiting.indexOf(wake), 1)
resolve(false)
}
const wake = () => {
deadline.removeEventListener("abort", giveUp)
resolve(true)
}
deadline.addEventListener("abort", giveUp, { once: true })
compileCheckWaiting.push(wake)
})
// 被叫醒时名额是直接转交过来的,compileChecks 不用再加
if (!admitted) return false
}
try {
const response = await postJudge(language, source, 1000, 256, [{ input: "", output: "" }], {
signal: deadline,
return await withJudgeSlot("interactive", { holdMs: 30_000, signal: deadline }, async () => {
const response = await postJudge(language, source, 1000, 256, [{ input: "", output: "" }], {
signal: deadline,
})
const body = (await response.json()) as JudgeResponse
return body.err === "CompileError"
})
const body = (await response.json()) as JudgeResponse
return body.err === "CompileError"
} catch {
return false
} finally {
const next = compileCheckWaiting.shift()
if (next) next()
else compileChecks--
}
}
@@ -210,13 +187,16 @@ async function requestJudge(
) {
const timeoutMs = judgeTimeoutMs(timeLimit)
try {
const response = await postJudge(language, code, timeLimit, memoryLimit, testCase, {
output,
signal: AbortSignal.timeout(timeoutMs),
// 超时从拿到名额才开始算:排队等名额不算判题机没回应
return await withJudgeSlot("judge", { holdMs: timeoutMs + 30_000 }, async () => {
const response = await postJudge(language, code, timeLimit, memoryLimit, testCase, {
output,
signal: AbortSignal.timeout(timeoutMs),
})
const body = await readCapped(response, MAX_RESPONSE_BYTES)
if (body === null) throw new Error("判题机返回的内容太大")
return JSON.parse(body) as JudgeResponse
})
const body = await readCapped(response, MAX_RESPONSE_BYTES)
if (body === null) throw new Error("判题机返回的内容太大")
return JSON.parse(body) as JudgeResponse
} catch (error) {
// 这句会写进 err_info,学生能看到,别把 "The operation timed out." 原样给他
if (error instanceof DOMException && error.name === "TimeoutError") {