fix(P1-async): 预测改为异步 POST+轮询,避免网关超时(Cloudflare 524)
后端:POST /predict 立即返回 job_id,后台 asyncio.create_task 执行;
新增 GET /predict/jobs/{job_id} 轮询状态(running/success/failed)。
前端: useMatchPredict 改为 POST 拿 job_id → 每 3s 轮询直到终态;
整体超时 5 分钟不变。避免多专家预测 60-180s 触发 Cloudflare 100s 超时(HTTP 524)。
This commit is contained in:
@@ -11,6 +11,8 @@ import { useRef, useState } from 'react'
|
||||
import { http } from '../../../lib/http'
|
||||
import type { Match, Prediction } from '../types'
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms))
|
||||
|
||||
/** 把后端/网络错误翻译成用户可读文案 */
|
||||
function readablePredictError(e: unknown): string {
|
||||
if (e instanceof Error) {
|
||||
@@ -59,17 +61,43 @@ export function useMatchPredict({ onError }: UseMatchPredictOptions) {
|
||||
onError(null)
|
||||
setPrediction(null)
|
||||
setPredictionFor(m)
|
||||
// LLM 多专家预测耗时可达数分钟,给足超时(与 nginx 代理 300s 对齐)
|
||||
|
||||
// P1-async: 预测改为异步,POST 立即返回 job_id,轮询结果避免网关超时(Cloudflare 100s → 524)
|
||||
const controller = new AbortController()
|
||||
predictAbort.current = controller
|
||||
const timer = setTimeout(() => controller.abort(), 300_000)
|
||||
const overallTimer = setTimeout(() => controller.abort(), 300_000)
|
||||
|
||||
try {
|
||||
const data = await http.post<Prediction>('/predict', { match_id: m.id, mode: 'multi' }, {
|
||||
timeoutMs: 300_000,
|
||||
signal: controller.signal,
|
||||
})
|
||||
// 1) 发起预测,拿到 job_id
|
||||
const started = await http.post<{ job_id: string; poll_url: string }>(
|
||||
'/predict',
|
||||
{ match_id: m.id, mode: 'multi' },
|
||||
{ timeoutMs: 10_000, signal: controller.signal },
|
||||
)
|
||||
if (seq !== predictSeq.current) return
|
||||
setPrediction(data)
|
||||
|
||||
// 2) 轮询直到终态(success/failed)或整体超时
|
||||
const deadline = Date.now() + 300_000
|
||||
while (Date.now() < deadline) {
|
||||
if (controller.signal.aborted) throw new DOMException('aborted', 'AbortError')
|
||||
await sleep(3000)
|
||||
const job = await http.get<{ status: string; result?: Prediction; error?: string }>(
|
||||
`/predict/jobs/${started.job_id}`,
|
||||
{ timeoutMs: 5000, signal: controller.signal },
|
||||
)
|
||||
if (seq !== predictSeq.current) return
|
||||
if (job.status === 'success') {
|
||||
setPrediction(job.result ?? null)
|
||||
return
|
||||
}
|
||||
if (job.status === 'failed') {
|
||||
onError(job.error || '预测失败')
|
||||
return
|
||||
}
|
||||
// status === 'running' → 继续轮询
|
||||
}
|
||||
// 整体超时
|
||||
onError('预测超时(5 分钟),请稍后重试')
|
||||
} catch (e) {
|
||||
if (seq !== predictSeq.current) return
|
||||
onError(
|
||||
@@ -78,7 +106,7 @@ export function useMatchPredict({ onError }: UseMatchPredictOptions) {
|
||||
: readablePredictError(e),
|
||||
)
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
clearTimeout(overallTimer)
|
||||
if (seq === predictSeq.current) setPredictingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user