提交前先判预算,耗尽时不再发起计费的 submit

- run_model3d_job 在没有 checkpoint 的分支上先走 ensure_submit_budget:
  预算已被解析 / 排队耗光时直接按 provider 超时终态失败,不去白花一次 provider 提交
- 续跑(已有 checkpoint)分支不受影响,仍然只轮询不提交
- 新增用例:预算耗尽必须拒绝提交,仍有预算必须放行
This commit is contained in:
2026-09-23 17:45:22 +08:00
parent 42b4315b4f
commit 85219ad1d4
@@ -80,7 +80,13 @@ async fn run_model3d_job(
let client = tripo_provider_client(&state.config)?;
let handle = match existing_checkpoint(job) {
Some(task_id) => TripoTaskHandle { task_id },
None => submit_and_checkpoint(state, caller, job, &client, &request).await?,
None => {
// submit 是不可逆且计费的调用:预算可能在别处就先耗光了(图生 3D 解析与上传
// 大图、任务排在其他 job 之后),这时提交上去也只会立刻被 deadline 判失败,
// 白花一次 provider 调用。只有还轮询得起才提交。
ensure_submit_budget(provider_deadline)?;
submit_and_checkpoint(state, caller, job, &client, &request).await?
}
};
let snapshot = poll_until_terminal(&client, &handle, provider_deadline).await?;
let model = read_artifact(
@@ -134,6 +140,14 @@ fn existing_checkpoint(job: &ExternalGenerationJobRecord) -> Option<String> {
.map(ToOwned::to_owned)
}
/// 提交前的预算检查:本次尝试已经没有轮询预算时,不发起不可逆且计费的 provider submit。
fn ensure_submit_budget(provider_deadline: Instant) -> Result<(), AppError> {
if Instant::now() >= provider_deadline {
return Err(provider_deadline_error());
}
Ok(())
}
async fn submit_and_checkpoint(
state: &AppState,
caller: &EditorGenerationCaller,
@@ -465,6 +479,18 @@ mod tests {
use crate::tripo3d::job::Model3dJobKind;
use module_assets::{AssetObjectAccessPolicy, AssetObjectUpsertInput};
/// 预算已经耗尽时不能再提交:submit 不可逆且计费,提交上去也只会立刻被 deadline 判失败。
#[test]
fn submit_is_rejected_once_the_polling_budget_is_gone() {
let expired = ensure_submit_budget(Instant::now())
.expect_err("预算已耗尽的尝试不能再发起 provider submit");
assert_eq!(expired.status_code(), StatusCode::BAD_GATEWAY);
assert!(
ensure_submit_budget(Instant::now() + Duration::from_secs(1)).is_ok(),
"还有预算时必须放行"
);
}
/// at-most-once submit 完全依赖这条判定:checkpoint 有值就只能续跑查询。
/// 空白值必须等同于“没有 checkpoint”,否则一次空写入会把 job 永久锁死。
#[test]