走进 Leverage OJ / 第 10 课
一次提交如何排队并进入评测
从 HTTP 路由开始,逐步追踪 DTO、Service、数据库、队列、Worker 与最终评测结果。
沿着固定版本的 POST /submissions 走一遍。用户在浏览器提交题目、代码、语言,例如 { "problemId": 42, "code": "print(1 + 1)", "language": 9 }。这只是 JSON 正文;请求还需要登录凭证。Controller 上的 JwtAuthGuard 先验证身份,随后从登录用户取得 ID,并把它与 CreateSubmissionDto 交给 SubmissionService.create。用户 ID 来自身份而非客户端随意指定的正文。
请求先通过 DTO 与全局 ValidationPipe:题目 ID 是至少为 1 的整数,代码是非空字符串且最长 65536 个字符,语言字段必须是数字;contestId、courseId 可以省略,若提供则须为整数。注意这只是格式层的检查。Service 接着执行频率限制、确认题目存在,并用支持语言表检查语言 ID;公开版本的 Python ID 为 9。数字 999 虽符合 @IsNumber(),仍会被 Service 拒绝。
通过检查后,Service 将提交基本信息存进 Submission,初始状态为 PENDING,并生成新的 judgeAttempt 标识本次评测尝试;提交代码另存到关联的 SubmissionMisc。随后 dispatch 把 { submissionId, attemptId } 送入 JUDGE_TX_QUEUE,任务名为 internal-submission。任务刻意只携带身份,不复制代码;数据库才是提交来源。入队失败时,代码会尝试将其结算为系统错误,再抛出异常。PENDING 意味着等待评测,不是已经通过。
Worker 在队列中领取任务后调用评测服务。服务按提交 ID、内部 provider 与 attemptId 读取记录,再读取题目和代码;因此它不用相信队列消息带来的代码正文。它将状态推进到 COMPILING,调用评测引擎;进入运行阶段可变为 JUDGING。任务标识用于辨认过期尝试,避免旧工作把新结果写回。完成后 ReceiveService.finalize 检查结果为终态,再更新 SQL 中的状态、用时、内存、评测信息与统计,并发布排名更新。
浏览器得到创建提交的响应后,可以再读取提交详情或状态。请求被接收、任务排队、程序编译和运行、结果结算,是分开的阶段;因此“接口成功”不等于“答案 AC”。这也是为什么要让用户先看到提交编号和待处理状态,再获取最终结果。
小练习
为什么队列任务不直接携带整段代码?如果第一次尝试过期,又启动新评测,attemptId 有什么用?
**答案:**Worker 按提交 ID 从数据库取权威代码,任务只需标识要处理的记录。不同尝试有不同 ID,过期任务不能冒充当前尝试完成结算。
去真实仓库看看
以下链接固定在公开提交 de34b268e75f293e904d7734c194a4ff2467b7a8,避免引用本地未提交的字符串语言 DTO:
互动练习:一次提交的处理流程
按请求进入应用后的常见处理顺序排列步骤。点击步骤可调整顺序。
点击步骤将它移到队尾。
提交请求先经过身份与数据校验,再保存记录、把任务身份放入队列,Worker 读取权威数据并完成评测。