fix(判题): 任务被遗弃时给提交落一个终态,别永远停在「等待评分」

judgeSubmission 自己的 try/catch 已经把正常路径上的异常都接住、落成 SYSTEM_ERROR
了,但队列的 failed 事件只有一行 console.error。判题队列没配 attempts(flowchart
队列配了 3),失败即终局;worker 进程被杀那种 —— 机房断电、容器 OOM、部署重启 ——
BullMQ 走完 stalled 重入队还是没人接,最后 emit failed,那条提交就永远停在
「等待评分」:学生看着转圈,教师统计里还占着一个「判题中」的名额。

加 failAbandonedSubmission(),在 failed 里调用,复用已有的 markSystemError(只动
PENDING/JUDGING,判完的和重判过的都不会被覆盖)。三种情况实跑验过:不存在的提交
安全返回、卡住的那条变成 result=5 并写入 err_info、已 AC 的那条没被动。

生产库里 3 条卡死的 PENDING(2022-11 / 2026-03 / 2026-04)都是旧栈时代留下的,
OJ2 上没有实例 —— 这次是把口子堵上,不是修已发生的故障。那 3 行和跟着差 1 的三道题
计数器还得手工处理。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqjE6qPo67fqVDKn6Bx7yd
This commit is contained in:
2026-09-08 18:29:30 -06:00
parent 64ad6139f5
commit a5b57d8ab8
2 changed files with 36 additions and 2 deletions

View File

@@ -325,6 +325,31 @@ async function markSystemError(submissionId: string, userId: number, error: unkn
}
}
/**
* 判题任务在 `judgeSubmission` 之外失败时的兜底。
*
* 正常路径上的异常都被 judgeSubmission 自己的 try/catch 接住、落成 SYSTEM_ERROR
* 并且推给前端,所以能走到队列 `failed` 事件的只剩两种:取提交那一步就炸了,
* 以及**worker 进程中途死掉** —— 机房断电、容器 OOM 被杀、部署时重启。后一种
* BullMQ 会先按 stalled 重入队一次,再没人接就彻底放手;判题队列又没配 attempts
* 失败即终局。没有这个兜底,那条提交就永远停在「等待评分」,学生看着转圈,
* 教师统计里它还占着一个「判题中」的名额。生产库里 3 条卡死的 PENDING
* 2022-11 / 2026-03 / 2026-04都是旧栈时代留下的就是这么来的。
*
* `markSystemError` 只动 PENDING / JUDGING 两个状态,所以判完了的、被重判改过的
* 都不会被它覆盖。唯一能撞上的是「重判刚把状态置回 PENDING同一刻上一个被遗弃的
* 任务才失败」——结果是这次重判被吃掉、显示成系统错误,比静默卡死看得见。
*/
export async function failAbandonedSubmission(submissionId: string, error: unknown) {
const [row] = await db
.select({ userId: schema.submission.userId })
.from(schema.submission)
.where(eq(schema.submission.id, submissionId))
.limit(1)
if (!row) return
await markSystemError(submissionId, row.userId, error)
}
export async function judgeSubmission(job: JudgeJobData) {
const [row] = await db
.select({

View File

@@ -2,7 +2,7 @@ import { Worker } from "bullmq"
import { config } from "./config"
import { judgeQueueName, type JudgeJobData } from "./judge/job"
import { judgeSubmission } from "./judge/run"
import { failAbandonedSubmission, judgeSubmission } from "./judge/run"
import { flowchartQueueName, type FlowchartJobData } from "./flowchart/job"
import { evaluateFlowchart } from "./flowchart/run"
import { createBlockingRedis } from "./redis"
@@ -29,8 +29,17 @@ const flowchartWorker = new Worker<FlowchartJobData>(
worker.on("ready", () => {
console.log(`Judge worker ready (concurrency=${config.judgeConcurrency})`)
})
worker.on("failed", (job, error) => {
worker.on("failed", async (job, error) => {
console.error(`Judge job ${job?.id ?? "unknown"} failed`, error)
// 队列没配 attempts失败即终局worker 被杀掉那种 BullMQ 走完 stalled 重试也会
// 落到这里。不在这里写一个终态,提交就永远停在「等待评分」,没有任何人会再管它。
const submissionId = job?.data.submissionId
if (!submissionId) return
try {
await failAbandonedSubmission(submissionId, error)
} catch (markError) {
console.error(`Failed to mark submission ${submissionId} as system error`, markError)
}
})
worker.on("error", (error) => {
console.error("Judge worker error", error)