perf(判题结果): 全班一起交、判题排队时不再每人每 2 秒问一次结果
Deploy / deploy (push) Canceled after 0s

原来交上去 5 秒没结果就每 2 秒(流程图 3 秒)拉一次,分不清 WebSocket 断了还是队列长:
50 人一起交、队列排一分钟时每秒二十几个请求,全在和判题抢那两个核。

- 收到这一条的任何推送(服务端订阅时就回一帧当前状态)就认定 WebSocket 是通的,
  轮询退到 20 秒一次,只防推送丢失;连接断了才回快档,2 秒起按 1.5 倍退到 10 秒
- 拉失败不再永久停掉(掉线那几秒拉失败是常事),连着失败 5 次才放弃
- 页面在后台时不拉;比赛题目页、榜单、班级页的定时刷新也一样,切回来立刻补一次
- 代码判题和流程图评分共用 shared/composables/fallbackPoll.ts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-16 14:37:08 +08:00
1 parent 593a409fca
commit 64994b9499
7 files changed
+199 -105

No files matched your search

@@ -1,10 +1,10 @@
import { ref, computed, getCurrentScope, onScopeDispose } from "vue"
import { useIntervalFn, useTimeoutFn } from "@vueuse/core"
import { ref, computed } from "vue"
import { getSubmission } from "oj/api"
import { SubmissionStatus } from "utils/constants"
import type { PendingAchievement, Submission } from "utils/types"
import { useAchievementStore } from "shared/store/achievement"
import { useSubmissionWebSocket, type SubmissionUpdate } from "shared/composables/websocket"
import { useFallbackPoll } from "shared/composables/fallbackPoll"
/**
* 判题监控 Composable
@@ -15,33 +15,6 @@ export function useSubmissionMonitor() {
const submissionId = ref("")
const submission = ref<Submission>()
// ==================== 轮询机制 ====================
const { pause: pausePolling, resume: resumePolling } = useIntervalFn(
async () => {
const id = submissionId.value
if (!id) return
try {
const res = await getSubmission(id)
// 请求在路上时换了题(reset)或者又交了一次:回来的是上一条,扔掉。
// 不扔的话上一道题判对了,会把新题标成已解决、弹点评
if (id !== submissionId.value) return
submission.value = res
const result = res.result
// 判题完成,停止轮询
if (result !== SubmissionStatus.judging && result !== SubmissionStatus.pending) {
pausePolling()
}
} catch (error) {
console.error("[SubmissionMonitor] 轮询失败:", error)
pausePolling()
}
},
2000,
{ immediate: false },
)
// ==================== WebSocket 处理 ====================
const handleSubmissionUpdate = (data: SubmissionUpdate) => {
// push_to_user 复用了 submission_update 这个 channel handler,
@@ -64,6 +37,8 @@ export function useSubmissionMonitor() {
console.log("[SubmissionMonitor] 提交ID不匹配,忽略")
return
}
// 订阅后服务端立刻回的那一帧(还在排队也回)就算:WebSocket 是通的,轮询退到慢档
fallback.confirm()
if (!submission.value) {
submission.value = {} as Submission
@@ -76,7 +51,7 @@ export function useSubmissionMonitor() {
console.log(`[SubmissionMonitor] 判题${data.status === "finished" ? "完成" : "出错"}`)
// 停止轮询(WebSocket已成功)
pausePolling()
fallback.stop()
// 结果已经到手,别让重连再去重放这条早就判完的订阅
unsubscribe()
@@ -91,25 +66,23 @@ export function useSubmissionMonitor() {
}
// 初始化 WebSocket
const { connect, subscribe, unsubscribe, scheduleDisconnect, cancelScheduledDisconnect } =
const { connect, subscribe, unsubscribe, scheduleDisconnect, cancelScheduledDisconnect, status } =
useSubmissionWebSocket(handleSubmissionUpdate)
// ==================== 轮询保底启动 ====================
const { start: startPollingFallback } = useTimeoutFn(
() => {
if (
submission.value &&
(submission.value.result === SubmissionStatus.judging ||
submission.value.result === SubmissionStatus.pending ||
submission.value.result === SubmissionStatus.submitting)
) {
console.log("[SubmissionMonitor] WebSocket未及时响应,启动轮询保底")
resumePolling()
}
// ==================== 轮询兜底 ====================
const fallback = useFallbackPoll({
status,
poll: async () => {
const id = submissionId.value
if (!id) return true
const res = await getSubmission(id)
// 请求在路上时换了题(reset)或者又交了一次:回来的是上一条,扔掉。
// 不扔的话上一道题判对了,会把新题标成已解决、弹点评
if (id !== submissionId.value) return false
submission.value = res
return res.result !== SubmissionStatus.judging && res.result !== SubmissionStatus.pending
},
5000,
{ immediate: false },
)
})
// ==================== 启动监控 ====================
const startMonitoring = (id: string) => {
@@ -131,8 +104,8 @@ export function useSubmissionMonitor() {
// 把各自那个早就判完的旧 submissionId 重新订阅一遍。
subscribe(id)
// 5秒后启动轮询保底(防止WebSocket失败)
startPollingFallback()
// 轮询兜底:WebSocket 通的话 20 秒才问一次,不通才快拉(见 fallbackPoll.ts)
fallback.start()
}
/**
@@ -141,7 +114,7 @@ export function useSubmissionMonitor() {
* 判完触发的「通过」处理会把**新题**标成已解决。提交本身照常判完落库,只是这边不再跟。
*/
const reset = () => {
pausePolling()
fallback.stop()
unsubscribe()
// 连接由 store 持有,离开题目页组件卸了它也还在。和判完之后一样,空闲 15 分钟断开;
// 不在这里立刻断,是因为刚交的那条判完之后还可能推成就过来
@@ -161,11 +134,6 @@ export function useSubmissionMonitor() {
return judging.value || pending.value || submitting.value
})
// ==================== 清理 ====================
// 现在由 submission store 持有(见 oj/store/submission.ts),store 不销毁就一直在;
// 用 onScopeDispose 是为了在组件里调用时照样能收尾
if (getCurrentScope()) onScopeDispose(pausePolling)
return {
// 状态
submissionId,
@@ -179,7 +147,6 @@ export function useSubmissionMonitor() {
// 方法
startMonitoring,
pausePolling,
reset,
}
}