From 56b65a430d1671ecbc273ebe297d0871e75ecc66 Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 31 Jul 2026 05:40:51 +0000 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=8C=81=E4=B9=85=E5=8C=96=E7=9A=84=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OSS 共享客户端扩展为读写共用并按上传方向放宽整体超时 画板生成图片持久化的 PUT 与 HEAD 改走共享客户端 补齐持久化写路径的共享客户端回归断言 Co-Authored-By: Claude Opus 5 --- .../crates/api-server/src/editor_project.rs | 29 ++++++++++++--- server-rs/crates/api-server/src/state.rs | 36 ++++++++++--------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 8f2dac4e0..1646aba75 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -8614,14 +8614,18 @@ async fn persist_editor_generated_image_data( })) })?; let persisted_mime_type = prepared.format.mime_type.clone(); - let http_client = reqwest::Client::new(); + // 中文注释:写路径此前也是每次新建 client,PUT 与 HEAD 都没有超时——请求可以在 + // 上传阶段无限期挂住,而这一段发生在 CPU 处理之后,任何按处理预算派生的 deadline + // 都已经不适用(余额多半为零,传进来只会把算完的结果丢掉)。这里的界只能来自 + // 客户端级超时,与读路径共用同一个进程级客户端。 + let http_client = state.editor_oss_http_client(); let put_result = oss_client - .put_object(&http_client, prepared.request) + .put_object(http_client, prepared.request) .await .map_err(|error| map_oss_error(error, "aliyun-oss"))?; let head = oss_client .head_object( - &http_client, + http_client, OssHeadObjectRequest { object_key: put_result.object_key.clone(), }, @@ -9025,7 +9029,7 @@ async fn read_editor_reference_image_object( // 中文注释:共享客户端自带 connect / total 超时,GET 不再可能无界挂起; // 顺带复用连接池,避免每次读参考图都重新做一次 TLS 握手。 let mut response = state - .editor_oss_read_http_client() + .editor_oss_http_client() .get(signed.signed_url.as_str()) .send() .await @@ -14050,7 +14054,7 @@ mod tests { "bytes.extend_from_slice", // 中文注释:必须走进程级共享客户端。它自带 connect / total 超时, // 是所有 OSS 读取调用方(含没有 deadline 可传的降级路径)的兜底上界。 - ".editor_oss_read_http_client()", + ".editor_oss_http_client()", ], ); assert_function_not_contains( @@ -14064,6 +14068,21 @@ mod tests { "reqwest::Client::new()", ], ); + // 中文注释:写路径与读路径同一条不变式。persist 发生在 CPU 处理之后,处理预算 + // 已经不适用,客户端级超时是它唯一的界;退回裸客户端会让 PUT / HEAD 重新无界, + // 而这一段是全部 9 条编辑器图片持久化流程共用的。 + assert_function_contains( + source, + "async fn persist_editor_generated_image_data", + "async fn persist_editor_provider_source_image", + &["state.editor_oss_http_client()"], + ); + assert_function_not_contains( + source, + "async fn persist_editor_generated_image_data", + "async fn persist_editor_provider_source_image", + &["reqwest::Client::new()"], + ); } #[test] diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index 60a1008cc..af376db99 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -272,7 +272,7 @@ pub struct AppStateInner { bgfilter_image_validation_limiter: Arc, character_animation_oss_http_client: reqwest::Client, character_animation_oss_io_limiter: Arc, - editor_oss_read_http_client: reqwest::Client, + editor_oss_http_client: reqwest::Client, #[cfg(any())] creative_agent_executor: Arc, // Phase 1 任务 E 的 creative session facade 暂存在 api-server。 @@ -531,7 +531,7 @@ impl AppState { let character_animation_oss_http_client = build_character_animation_oss_http_client()?; let character_animation_oss_io_limiter = Arc::new(Semaphore::new(CHARACTER_ANIMATION_OSS_MAX_CONCURRENCY)); - let editor_oss_read_http_client = build_editor_oss_read_http_client()?; + let editor_oss_http_client = build_editor_oss_http_client()?; let http_request_permit_pools = HttpRequestPermitPools::from_config(&config); let (profile_recharge_order_updates, _) = broadcast::channel(128); @@ -579,7 +579,7 @@ impl AppState { bgfilter_image_validation_limiter, character_animation_oss_http_client, character_animation_oss_io_limiter, - editor_oss_read_http_client, + editor_oss_http_client, #[cfg(any())] creative_agent_executor: Arc::new(MockLangChainRustAgentExecutor), #[cfg(any())] @@ -1320,8 +1320,8 @@ impl AppState { self.character_animation_oss_io_limiter.clone() } - pub fn editor_oss_read_http_client(&self) -> &reqwest::Client { - &self.editor_oss_read_http_client + pub fn editor_oss_http_client(&self) -> &reqwest::Client { + &self.editor_oss_http_client } #[cfg(any())] @@ -2096,23 +2096,27 @@ fn build_character_animation_oss_http_client() -> Result Result { +// total 取 120s:按较慢的写方向定尺寸——32 MiB 上传在 60s 内要求持续约 4.4 Mbps, +// 留一倍余量避免误伤正常流量。读方向不因此变松:完美像素的 GET 另受 30s 处理预算约束, +// 客户端超时只是兜底。 +fn build_editor_oss_http_client() -> Result { reqwest::Client::builder() .connect_timeout(std::time::Duration::from_secs(10)) - .timeout(std::time::Duration::from_secs(60)) + .timeout(std::time::Duration::from_secs(120)) .pool_idle_timeout(std::time::Duration::from_secs(300)) .pool_max_idle_per_host(8) .tcp_keepalive(std::time::Duration::from_secs(60)) .build() .map_err(|error| { AppStateInitError::DependencyUnavailable(format!( - "初始化编辑器 OSS 读取 HTTP 客户端失败:{error}" + "初始化编辑器 OSS HTTP 客户端失败:{error}" )) }) } @@ -2265,12 +2269,12 @@ mod tests { } #[test] - fn app_state_reuses_editor_oss_read_client() { + fn app_state_reuses_editor_oss_client() { let state = AppState::new(AppConfig::default()).expect("state should build"); assert!(std::ptr::eq( - state.editor_oss_read_http_client(), - state.editor_oss_read_http_client(), + state.editor_oss_http_client(), + state.editor_oss_http_client(), )); }