消除像素规整降级路径的重复与无界 OSS 回读
Project CI / Repository checks (pull_request) Successful in 1m0s
Project CI / Frontend tests (pull_request) Successful in 2m1s
Project CI / Native shell tests (pull_request) Successful in 2m26s
Project CI / Backend tests (pull_request) Successful in 3m28s

尺寸守卫此前一律走会回读 provider 原图的包装函数,带来两个问题:permit
获取失败、worker 内 deadline、join 异常和 worker 超时这四条分支其实已经
持有原图,却又对同一 object key 发起第二次 GET,把"最多增加一次 OSS GET"
放大成两次;而第一次回读超时后的那次 GET 完全没有绝对 deadline,突破
30 秒像素预算。

拆成两类:已持有原图的四条分支改走纯内存守卫 degrade_editor_pixel_art_
with_provider_source,零额外 GET;尚未取得原图的三条分支仍走回读守卫,
但该次 GET 改以外层请求 deadline 为绝对上界——用已耗尽的像素预算绑会让
修复必然失败,不绑则突破预算。

apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original
新增可选 download_deadline,非像素路径传 None 保持既有语义;尺寸比对抽为
editor_postprocessed_alpha_matches_delivery_dimensions 供两类守卫共用。
计数断言固定"回读守卫 3 处、内存守卫 4 处",该断言在编写时即抓出一处
分支数误判。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 12:29:42 +00:00
parent 414735efde
commit 937378ab91
2 changed files with 173 additions and 65 deletions
@@ -23,6 +23,7 @@
- 影响范围:`server-rs/crates/api-server/src/editor_project.rs` 的角色与图标像素规整降级路径;不改变成功路径、OSS PUT 次数、资源类型、画布项或前端契约,OSS GET 仍只在尺寸漂移时发生。
- 验证方式:`pixel_art_degrade_paths_guard_postprocessed_delivery_dimensions` 结构断言固定"降级分支不得返回 `(postprocessed, None, …)`"与守卫的委托实现;运行 `cargo test -p api-server editor_project --manifest-path server-rs/Cargo.toml``npm run check:rustfmt``npm run check:encoding``git diff --check`
- 关联文档:本文件「2026-07-29 角色带背景原图与透明图统一交付尺寸」与「2026-07-28 图片生成风格使用可扩展字段并以纯内存像素规整首发」。
- 补充(同日):守卫的回读必须分两类处理,否则会把「最多增加一次 OSS GET」放大成两次、且第二次无界。已取得 provider 原图的四条降级分支(permit 获取失败、worker 内 deadline、join 异常、worker 超时)改走纯内存守卫 `degrade_editor_pixel_art_with_provider_source`,零额外 GET;尚未取得原图的三条分支(进函数即预算耗尽、第一次回读失败、第一次回读超时)才走会回读的守卫,且该次 GET 以**外层请求 deadline**(而非已耗尽的像素预算)为绝对上界——用像素预算绑会让修复必然失败,不绑则突破预算。`apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original` 因此新增可选 `download_deadline`,非像素路径传 `None` 保持既有语义不变。计数断言固定「回读守卫 3 处、内存守卫 4 处」,防止后续新增分支时误用回读版本。
---
+172 -65
View File
@@ -2042,6 +2042,7 @@ pub(crate) async fn generate_editor_image_for_owner(
delivery_width,
delivery_height,
removal.image,
None,
)
.await;
(image, error, None)
@@ -3082,14 +3083,24 @@ fn apply_editor_postprocessed_alpha_to_provider_source_with_policy(
}))
}
async fn apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original(
state: &AppState,
provider_source_object_key: &str,
provider_width: u32,
provider_height: u32,
// 中文注释:已经持有 provider 原图时的纯内存尺寸守卫,不发起任何 OSS 请求。
fn apply_editor_postprocessed_alpha_with_provider_source_or_original(
provider_source: &DownloadedOpenAiImage,
postprocessed: DownloadedOpenAiImage,
) -> (DownloadedOpenAiImage, Option<AppError>) {
let postprocessed_dimensions =
match apply_editor_postprocessed_alpha_to_provider_source(provider_source, &postprocessed) {
Ok(Some(restored)) => (restored, None),
Ok(None) => (postprocessed, None),
Err(error) => (postprocessed, Some(error)),
}
}
fn editor_postprocessed_alpha_matches_delivery_dimensions(
postprocessed: &DownloadedOpenAiImage,
provider_width: u32,
provider_height: u32,
) -> Result<bool, AppError> {
let (postprocessed_width, postprocessed_height) =
image::ImageReader::new(Cursor::new(postprocessed.bytes.as_slice()))
.with_guessed_format()
.map_err(|error| {
@@ -3105,28 +3116,55 @@ async fn apply_editor_postprocessed_alpha_from_persisted_provider_source_or_orig
"message": format!("无法读取透明后处理图尺寸:{error}"),
}))
})
});
match postprocessed_dimensions {
Ok((postprocessed_width, postprocessed_height))
if postprocessed_width == provider_width && postprocessed_height == provider_height =>
{
return (postprocessed, None);
}
Ok(_) => {}
})?;
Ok(postprocessed_width == provider_width && postprocessed_height == provider_height)
}
async fn apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original(
state: &AppState,
provider_source_object_key: &str,
provider_width: u32,
provider_height: u32,
postprocessed: DownloadedOpenAiImage,
download_deadline: Option<Instant>,
) -> (DownloadedOpenAiImage, Option<AppError>) {
match editor_postprocessed_alpha_matches_delivery_dimensions(
&postprocessed,
provider_width,
provider_height,
) {
Ok(true) => return (postprocessed, None),
Ok(false) => {}
Err(error) => return (postprocessed, Some(error)),
}
// 中文注释:provider 原图已在 BgFilter 调用前交给 OSS 并释放;只有输出尺寸漂移时才短时回读原图回贴 alpha。
let provider_source =
match download_editor_persisted_image_object(state, provider_source_object_key).await {
Ok(provider_source) => provider_source,
Err(error) => return (postprocessed, Some(error)),
};
match apply_editor_postprocessed_alpha_to_provider_source(&provider_source, &postprocessed) {
Ok(Some(restored)) => (restored, None),
Ok(None) => (postprocessed, None),
Err(error) => (postprocessed, Some(error)),
}
// download_deadline 为像素规整降级路径提供绝对上界,避免在预算已耗尽后仍发起无界 GET;
// 非像素路径沿用既有语义传 None。
let download = download_editor_persisted_image_object(state, provider_source_object_key);
let downloaded = match download_deadline {
Some(deadline) => {
match tokio::time::timeout_at(tokio::time::Instant::from_std(deadline), download).await
{
Ok(downloaded) => downloaded,
Err(_) => Err(
AppError::from_status(StatusCode::GATEWAY_TIMEOUT).with_details(json!({
"provider": "editor-image-postprocess",
"message": "回读 provider 原图超时,已保留透明后处理图。",
})),
),
}
}
None => download.await,
};
let provider_source = match downloaded {
Ok(provider_source) => provider_source,
Err(error) => return (postprocessed, Some(error)),
};
apply_editor_postprocessed_alpha_with_provider_source_or_original(
&provider_source,
postprocessed,
)
}
fn take_arc_downloaded_image(image: Arc<DownloadedOpenAiImage>) -> DownloadedOpenAiImage {
@@ -3221,8 +3259,12 @@ async fn degrade_editor_pixel_art_to_postprocessed_with_dimension_guard(
delivery_width: u32,
delivery_height: u32,
postprocessed: DownloadedOpenAiImage,
request_deadline: Option<Instant>,
reason: String,
) -> (DownloadedOpenAiImage, Option<AppError>, Option<String>) {
// 中文注释:只有尚未成功取得 provider 原图的降级分支才会走到这里。像素预算此时
// 多半已经耗尽,不能拿它去绑这次回读(否则必然失败、守卫形同虚设),改用外层
// 请求 deadline 作为绝对上界,保证这唯一一次 GET 有界。
let (image, dimension_error) =
apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original(
state,
@@ -3230,11 +3272,39 @@ async fn degrade_editor_pixel_art_to_postprocessed_with_dimension_guard(
delivery_width,
delivery_height,
postprocessed,
request_deadline,
)
.await;
(image, dimension_error, Some(reason))
}
// 中文注释:provider 原图已在内存里的降级分支走这里,零额外 OSS GET,
// 保证「最多因复用失败增加一次 GET」的不变式不被降级路径打破。
fn degrade_editor_pixel_art_with_provider_source(
provider_source: &DownloadedOpenAiImage,
delivery_width: u32,
delivery_height: u32,
postprocessed: DownloadedOpenAiImage,
reason: String,
) -> (DownloadedOpenAiImage, Option<AppError>, Option<String>) {
match editor_postprocessed_alpha_matches_delivery_dimensions(
&postprocessed,
delivery_width,
delivery_height,
) {
Ok(true) => (postprocessed, None, Some(reason)),
Ok(false) => {
let (image, dimension_error) =
apply_editor_postprocessed_alpha_with_provider_source_or_original(
provider_source,
postprocessed,
);
(image, dimension_error, Some(reason))
}
Err(error) => (postprocessed, Some(error), Some(reason)),
}
}
async fn apply_editor_postprocessed_alpha_and_pixel_art_from_persisted_provider_source_or_original(
state: &AppState,
provider_source_object_key: &str,
@@ -3255,6 +3325,7 @@ async fn apply_editor_postprocessed_alpha_and_pixel_art_from_persisted_provider_
delivery_width,
delivery_height,
postprocessed,
request_deadline,
"像素规整处理预算已耗尽,已保留透明后处理图。".to_string(),
)
.await;
@@ -3274,6 +3345,7 @@ async fn apply_editor_postprocessed_alpha_and_pixel_art_from_persisted_provider_
delivery_width,
delivery_height,
postprocessed,
request_deadline,
reason,
)
.await;
@@ -3285,6 +3357,7 @@ async fn apply_editor_postprocessed_alpha_and_pixel_art_from_persisted_provider_
delivery_width,
delivery_height,
postprocessed,
request_deadline,
"像素规整读取 provider 原图超时,已保留透明后处理图。".to_string(),
)
.await;
@@ -3293,15 +3366,13 @@ async fn apply_editor_postprocessed_alpha_and_pixel_art_from_persisted_provider_
let permit = match acquire_editor_pixel_art_cpu_permit(processing_deadline).await {
Ok(permit) => permit,
Err(error) => {
return degrade_editor_pixel_art_to_postprocessed_with_dimension_guard(
state,
provider_source_object_key,
return degrade_editor_pixel_art_with_provider_source(
&provider_source,
delivery_width,
delivery_height,
postprocessed,
error,
)
.await;
);
}
};
@@ -3356,39 +3427,27 @@ async fn apply_editor_postprocessed_alpha_and_pixel_art_from_persisted_provider_
Some(preparation_error),
Some(error),
),
Ok(Ok(Err((None, None, error)))) => {
degrade_editor_pixel_art_to_postprocessed_with_dimension_guard(
state,
provider_source_object_key,
delivery_width,
delivery_height,
take_arc_downloaded_image(postprocessed),
error,
)
.await
}
Ok(Err(error)) => {
degrade_editor_pixel_art_to_postprocessed_with_dimension_guard(
state,
provider_source_object_key,
delivery_width,
delivery_height,
take_arc_downloaded_image(postprocessed),
format!("像素规整工作线程异常:{error}"),
)
.await
}
Err(_) => {
degrade_editor_pixel_art_to_postprocessed_with_dimension_guard(
state,
provider_source_object_key,
delivery_width,
delivery_height,
take_arc_downloaded_image(postprocessed),
"像素规整处理超时,已保留透明后处理图。".to_string(),
)
.await
}
Ok(Ok(Err((None, None, error)))) => degrade_editor_pixel_art_with_provider_source(
provider_source.as_ref(),
delivery_width,
delivery_height,
take_arc_downloaded_image(postprocessed),
error,
),
Ok(Err(error)) => degrade_editor_pixel_art_with_provider_source(
provider_source.as_ref(),
delivery_width,
delivery_height,
take_arc_downloaded_image(postprocessed),
format!("像素规整工作线程异常:{error}"),
),
Err(_) => degrade_editor_pixel_art_with_provider_source(
provider_source.as_ref(),
delivery_width,
delivery_height,
take_arc_downloaded_image(postprocessed),
"像素规整处理超时,已保留透明后处理图。".to_string(),
),
}
}
@@ -4731,6 +4790,7 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
source_width,
source_height,
removal.image,
None,
)
.await;
(image, error, None)
@@ -5506,6 +5566,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner(
source_width,
source_height,
removal.image,
None,
)
.await;
if let Some(error) = postprocess_dimension_error {
@@ -9328,6 +9389,45 @@ mod tests {
"fallback.unwrap_or_else(",
],
);
// 中文注释:像素函数里只有「尚未取得 provider 原图」的三条分支(进函数即预算耗尽、
// 第一次回读失败、第一次回读超时)允许走会回读的守卫;取得原图之后的四条分支
// permit 获取失败、worker 内 deadline、join 异常、worker 超时)必须走纯内存守卫,
// 否则单次请求会对同一 object key 发出两次 GET,突破「最多增加一次 GET」的不变式。
let pixel_fn_start = source
.find("async fn apply_editor_postprocessed_alpha_and_pixel_art_from_persisted_provider_source_or_original(")
.expect("pixel art entry should exist");
let pixel_fn_body = &source[pixel_fn_start..];
let pixel_fn_body = &pixel_fn_body[..pixel_fn_body
.find("fn restore_editor_generated_image_output_dimensions_with_filter")
.expect("pixel art entry should be followed by the dimension restore helper")];
assert_eq!(
pixel_fn_body
.matches("degrade_editor_pixel_art_to_postprocessed_with_dimension_guard(")
.count(),
3,
"只有未取得 provider 原图的三条分支可以走回读守卫"
);
assert_eq!(
pixel_fn_body
.matches("degrade_editor_pixel_art_with_provider_source(")
.count(),
4,
"取得 provider 原图后的四条降级分支必须走纯内存守卫"
);
// 纯内存守卫本身不得触碰 OSS。
assert_function_not_contains(
source,
"fn degrade_editor_pixel_art_with_provider_source",
"async fn apply_editor_postprocessed_alpha_and_pixel_art_from_persisted_provider_source_or_original",
&["download_editor_persisted_image_object"],
);
// 会回读原图的守卫必须带绝对 deadline 参数,不允许无界 GET。
assert_function_contains_in_order(
source,
"async fn degrade_editor_pixel_art_to_postprocessed_with_dimension_guard",
"fn degrade_editor_pixel_art_with_provider_source",
&["request_deadline: Option<Instant>", "request_deadline,"],
);
// 守卫必须复用非像素路径的同一套尺寸比对与 alpha 回贴,保证尺寸一致时
// 不产生额外 OSS GET,漂移修不好时返回尺寸错误。
assert_function_contains_in_order(
@@ -12326,17 +12426,24 @@ mod tests {
"async fn persist_editor_provider_source_resource",
&["persist_editor_generated_image_owned"],
);
// 中文注释:尺寸一致时必须在回读 provider 原图之前就返回,只有漂移才发起 GET。
assert_function_contains_in_order(
source,
"async fn apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original",
"fn restore_editor_generated_image_output_dimensions",
&[
"into_dimensions",
"return (postprocessed, None)",
"editor_postprocessed_alpha_matches_delivery_dimensions",
"Ok(true) => return (postprocessed, None)",
"download_editor_persisted_image_object",
"apply_editor_postprocessed_alpha_to_provider_source",
"apply_editor_postprocessed_alpha_with_provider_source_or_original",
],
);
assert_function_contains_in_order(
source,
"fn editor_postprocessed_alpha_matches_delivery_dimensions",
"async fn apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original",
&["into_dimensions"],
);
for (start, end) in [
(
"pub(crate) async fn generate_editor_icon_spritesheet_for_owner",