fix(判题): 判题机卡住不再让全班一直「等待评分」,重判和同时交对也不再把做对题数算多
Deploy / deploy (push) Canceled after 0s

- 正式判题、样例重跑、运行时错误诊断都加了超时(默认 120 秒,时限放宽的题按比例放宽),
  超时记系统错误、提示是中文;回包和试运行一样边读边数,超过 16MB 就掐断,
  免得一个疯狂打印的错误程序把 worker 撑爆
- 做对题数、中等、困难、一次就对改成在锁里按定义现数:原来逐次 +1,两条挨着判完或重判
  一条做对的提交都会多加,库里 372 人多了 1–4 道。0035 把存量一次订正
- 重判不再给提交次数、凌晨/早起计数 +1,编译错误按前后差值改;「错了几次才对」只数这条之前的
- 已拿到的成就数只 jsonb_set 那一个键,不再整块写回冲掉并发判题刚写的指标

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-16 14:15:08 +08:00
1 parent 5aafcf172b
commit 55038cc78b
6 files changed
+4059 -70

No files matched your search

+55 -4
View File
@@ -164,6 +164,42 @@ export async function compileFails(language: string, source: string) {
}
}
/**
* 判题机回包的总上限。判题机给每组输出留 16MB(见镜像里 judge_client.py 的
* max_output_size),死循环打印能把每组都写满;api、worker 容器都只有 512MB,
* 几组全满读进来再 JSON.parse 一遍就是好几百 MB。超了就掐断。
*/
export const MAX_RESPONSE_BYTES = 16 * 1024 * 1024
/** 边读边数,超过上限就掐断连接,返回 null */
export async function readCapped(response: Response, limit: number) {
if (!response.body) return ""
const reader = response.body.getReader()
const chunks: Uint8Array[] = []
let size = 0
for (;;) {
const { done, value } = await reader.read()
if (done) break
size += value.byteLength
if (size > limit) {
await reader.cancel()
return null
}
chunks.push(value)
}
return Buffer.concat(chunks).toString("utf8")
}
/**
* 判题机自己有编译、运行的限时,这里只防它卡死不回。不设的话一次卡死就永久占住一个
* worker(服务器上一共两个),而 BullMQ 会一直给它续锁、不当它卡住,全班停在「等待评分」。
* 判题机每组的墙钟上限是 CPU 时限的 3 倍、两组并行,现有题目全是 1 秒,二十组全超时
* 也就半分钟出头,两分钟足够;时限放宽的题按比例放宽。
*/
function judgeTimeoutMs(timeLimit: number) {
return Math.max(120_000, timeLimit * 60)
}
async function requestJudge(
language: string,
code: string,
@@ -172,8 +208,22 @@ async function requestJudge(
testCase: string | { input: string; output: string }[],
output = false,
) {
const response = await postJudge(language, code, timeLimit, memoryLimit, testCase, { output })
return (await response.json()) as JudgeResponse
const timeoutMs = judgeTimeoutMs(timeLimit)
try {
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
} catch (error) {
// 这句会写进 err_info,学生能看到,别把 "The operation timed out." 原样给他
if (error instanceof DOMException && error.name === "TimeoutError") {
throw new Error(`判题机 ${timeoutMs / 1000} 秒没有回应`)
}
throw error
}
}
async function persistResult(
@@ -668,6 +718,7 @@ export async function judgeSubmission(job: JudgeJobData) {
}
}
const counted = countedBefore(job.rejudgedFrom)
const saved = await persistResult(
row.submission.id,
row.problem.id,
@@ -678,7 +729,7 @@ export async function judgeSubmission(job: JudgeJobData) {
statisticInfo,
row.submission.contestId,
row.submission.createTime,
countedBefore(job.rejudgedFrom),
counted,
)
if (!saved) return
@@ -692,7 +743,7 @@ export async function judgeSubmission(job: JudgeJobData) {
}
try {
const unlocked = await updateAchievementsForSubmission(row.submission.id)
const unlocked = await updateAchievementsForSubmission(row.submission.id, counted)
await publishAchievementNotification(
row.submission.userId,
unlocked.map((achievement) => ({