provider 报 unknown 任务状态时改为可重试,不再直接判死

- platform-tripo:`TaskStatus::Unknown` 不再归一成 `OutputSchema` 终态失败,改成新的 `TripoError::TaskStatusUnknown`,并把它的可重试性判为真(SDK 自己也没把 unknown 算进终态)
- platform-tripo:补用例——provider 回 `"status":"unknown"` 时必须返回可重试的 `TaskStatusUnknown`,不能当成正常快照
- api-server:错误映射补 `TaskStatusUnknown` 分支(502 + 固定文案,轮询侧早已按可重试继续查到截止时间)
- api-server worker:状态查询遇到可重试错误时补一条 warn 日志,便于区分「一直是 unknown」和「一次网络抖动」
- docs:Provider 集成方案写明 unknown 按可重试处理及其理由
This commit is contained in:
2026-09-24 21:06:04 +08:00
parent 2aa59328de
commit e20ff2e89e
6 changed files with 72 additions and 4 deletions
@@ -144,6 +144,15 @@ pub(crate) fn map_provider_error(error: TripoError) -> AppError {
"3D 生成任务的结果格式不符合预期,未落库。".to_string(),
)
}
// 轮询侧按可重试继续查到截止时间,真走到这里说明预估的时间已经用尽。
TripoError::TaskStatusUnknown { .. } => {
tracing::error!(error = %error, "tripo task status is unrecognized");
(
StatusCode::BAD_GATEWAY,
"tripo-task-status-unknown",
"3D 生成服务的任务状态暂时无法识别,请稍后重试。".to_string(),
)
}
// 配置问题正常在 `provider.rs` 就按 503 拦下;这里只是把枚举匹配补全,
// 真走到这一步说明有调用方绕过了构造入口,按服务不可用上报。
TripoError::Configuration { .. } => {
@@ -252,6 +252,9 @@ async fn poll_until_terminal(
// 查询本身已经带过一轮退避重试;仍是瞬时故障时继续按轮询间隔查到预算用尽,
// 不因为一次 provider 抖动就作废这笔已经提交、不能重来的任务。
Err(error) if error.is_retryable() => {
// 包括 provider 报的 `unknown` 状态:继续查到截止时间,日志留痕便于区分
// 「一直是 unknown」和「一次网络抖动」。
tracing::warn!(error = %error, "3D 生成任务状态查询遇到可重试错误,继续轮询");
let remaining = provider_deadline.saturating_duration_since(Instant::now());
tokio::time::sleep(MODEL3D_POLL_INTERVAL.min(remaining)).await;
continue;
@@ -522,6 +522,47 @@ mod tests {
assert_eq!(snapshot.status, Model3dTaskStatus::Running);
}
/// provider 报 `unknown` 状态:按可重试返回,不判终态失败(评审 #57)。
#[tokio::test]
async fn task_status_query_treats_unknown_status_as_retryable() {
let addr = spawn_mock_server(|mut socket| async move {
read_request(&mut socket).await;
let body = serde_json::json!({
"code": 0,
"data": {
"task_id": "task-1",
"type": "text_to_model",
"status": "unknown",
"progress": 10
}
})
.to_string();
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
Content-Length: {}\r\n\r\n{body}",
body.len()
);
let _ = socket.write_all(response.as_bytes()).await;
let _ = socket.flush().await;
})
.await;
let error = test_client_with_base_url(&format!("http://{addr}"), 0)
.get_task(&TripoTaskHandle {
task_id: "task-1".to_string(),
})
.await
.expect_err("认不出来的状态不能当成正常快照返回");
assert!(
matches!(error, TripoError::TaskStatusUnknown { .. }),
"{error:?}"
);
assert!(
error.is_retryable(),
"unknown 是暂时的看不懂,必须可重试:{error}"
);
}
/// 404 是确定性失败:只打一次请求就返回,不按退避重试。
#[tokio::test]
async fn task_status_query_does_not_retry_deterministic_failures() {
@@ -108,6 +108,14 @@ pub enum TripoError {
task_id: String,
message: String,
},
/// provider 返回了 `unknown` 这一档任务状态。
///
/// SDK 自己也不把 `unknown` 算作终态,因此这里按「暂时看不懂」处理,而不是终态失败:
/// Tripo job 固定只尝试一次、判死不可恢复,而 provider 以后新增状态时,旧代码会把
/// 仍在跑的任务整批判失败。归成可重试后,轮询侧会继续查到截止时间(评审 #57)。
TaskStatusUnknown {
task_id: String,
},
/// 客户端配置问题:字段由调用方提供,与请求参数、provider 响应都无关。
Configuration {
field: &'static str,
@@ -162,6 +170,9 @@ impl fmt::Display for TripoError {
Self::OutputSchema { task_id, message } => {
write!(f, "Tripo task {task_id} output schema error: {message}")
}
Self::TaskStatusUnknown { task_id } => {
write!(f, "Tripo task {task_id} reported the unknown task status")
}
Self::Configuration { field, message } => {
write!(f, "Tripo configuration error [{field}]: {message}")
}
@@ -414,6 +425,9 @@ impl TripoError {
match self {
Self::Api { status, .. } => matches!(status, Some(408 | 425 | 429 | 500..=599)),
Self::Request { retryable, .. } => *retryable,
// `unknown` 是「这次看不懂」,下次查询可能就看得懂;终态失败不可恢复,
// 所以判成可重试(评审 #57)。
Self::TaskStatusUnknown { .. } => true,
_ => false,
}
}
@@ -66,10 +66,11 @@ fn map_status(status: TaskStatus, task_id: &str) -> Result<Model3dTaskStatus, Tr
TaskStatus::Failed | TaskStatus::Banned => Ok(Model3dTaskStatus::Failed),
TaskStatus::Cancelled => Ok(Model3dTaskStatus::Cancelled),
TaskStatus::Expired => Ok(Model3dTaskStatus::Expired),
// 与其它映射失败一致带上 task id:provider 异常要能和具体任务对上。
TaskStatus::Unknown => Err(TripoError::OutputSchema {
// `unknown` 是「这次看不懂」,不是「任务结束了」:SDK 自己也没把它算进终态。
// 按可重试错误返回(评审 #57),由轮询侧按轮询间隔继续查到截止时间——判成终态
// 就等于把一笔已经提交、已经计费的任务永久作废,provider 以后新增状态时更会成批误杀。
TaskStatus::Unknown => Err(TripoError::TaskStatusUnknown {
task_id: task_id.into(),
message: "Tripo returned unknown task status".into(),
}),
}
}