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:
@@ -14,7 +14,7 @@
|
||||
|
||||
## 边界与状态
|
||||
|
||||
`platform-tripo` 是唯一接触 `tripo3d-sdk` 的边界。它返回自己的 provider DTO,不让 SDK 类型穿透到未来的 `api-server`。Tripo 的 `success` 映射为 `completed`;`failed`/`banned` 映射为 `failed`;`cancelled` 和 `expired` 保留为独立终态;未知状态返回结构化错误。
|
||||
`platform-tripo` 是唯一接触 `tripo3d-sdk` 的边界。它返回自己的 provider DTO,不让 SDK 类型穿透到未来的 `api-server`。Tripo 的 `success` 映射为 `completed`;`failed`/`banned` 映射为 `failed`;`cancelled` 和 `expired` 保留为独立终态;`unknown` 不在 SDK 的终态集合里,因此按「这次看不懂」处理,返回可重试的 `TripoError::TaskStatusUnknown`,由轮询侧继续查到截止时间 —— 判成终态失败不可恢复,而且 provider 以后新增状态时会把仍在跑的任务整批误杀。
|
||||
|
||||
provider task 结果与产品资源结果分离。provider adapter 不生成 `resourceId` 或 `assetId`;未来应用层在资源持久化后再构造带资源 ID 的产品 DTO。模型 URL 被视为临时 provider 引用,下载由显式方法完成。`TripoUrl` 的 `Debug` 与 `redacted()` 只输出 scheme、host 与 path,隐藏可能带签名的 query 与 fragment,并保留命中的分隔符(`?` 或 `#`),不把 fragment 显示成 query;带 userinfo 的地址在解析阶段直接拒绝;主机必须是公网地址,回环、内网、链路本地、组播与保留段(含云元数据地址 `169.254.169.254`)一律拒收,域名层面拦住 `localhost` 及其子域。域名解析出的真实地址在这一层看不见,域名指向内网的情况要靠连接层处理,属已知限制。完整 URL 仅通过 `as_str()` 显式读取;`TripoUrl` 不实现 `Display`,避免 `{}` 这类通用格式化把带签名的完整地址写进日志,冒烟示例同样只打印脱敏后的地址。
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user