消除像素规整降级路径的重复与无界 OSS 回读
尺寸守卫此前一律走会回读 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:
@@ -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 处」,防止后续新增分支时误用回读版本。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user