From c1f03d64d1af5ca576f5e9bec0e7ed32776fd0d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Fri, 4 Sep 2026 23:10:36 +0800 Subject: [PATCH] =?UTF-8?q?task-171:=20=E5=9B=BE=E7=89=87=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=AE=A2=E6=88=B7=E7=AB=AF=E9=85=8D=E7=BD=AE=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=EF=BC=88ImageDownloadHttpConfigResolver=EF=BC=8Cdownl?= =?UTF-8?q?oad/prefetch=20=E6=A8=A1=E5=9D=97=E7=BA=A7=E7=8E=B0=E6=9C=89?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=BC=98=E5=85=88=E3=80=81=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E5=85=9C=E5=BA=95=EF=BC=89+=208=20=E6=9D=A1?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 真实基线:SimilarAsinImageEmbedder(OkHttp) connect/read=imageDownloadTimeoutSeconds(5s)、call=2×download(10s)、prefetch 1800s - resolver 提供 connect/read/call/prefetch/maxRetries 访问面,模块级优先、命名空间兜底,不改调用点 - 仅新增类与测试,零生产行为变化 --- .../ImageDownloadHttpConfigResolver.java | 61 +++++++++++ .../ImageDownloadHttpConfigResolverTest.java | 100 ++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 backend-java/src/main/java/com/nanri/aiimage/config/ImageDownloadHttpConfigResolver.java create mode 100644 backend-java/src/test/java/com/nanri/aiimage/config/ImageDownloadHttpConfigResolverTest.java diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/ImageDownloadHttpConfigResolver.java b/backend-java/src/main/java/com/nanri/aiimage/config/ImageDownloadHttpConfigResolver.java new file mode 100644 index 00000000..7a17aab6 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/config/ImageDownloadHttpConfigResolver.java @@ -0,0 +1,61 @@ +package com.nanri.aiimage.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * 图片下载客户端统一配置解析(task-171)。 + * + * 真实基线(已核实,对应 task-166 契约表"图片下载"行):SimilarAsinImageEmbedder 使用自建 + * OkHttpClient(不参与共享 JDK HttpClientPool),connect/read/write 均取 + * aiimage.similar-asin.image-download-timeout-seconds(默认 5s),call = 2×download; + * 整批预取预算 image-prefetch-timeout-seconds(默认 1800s);无显式次数重试 + * (OkHttp retryOnConnectionFailure + 上游批次重提)。 + * + * 语义:模块级现有配置优先(保证接入切点时行为不变),命名空间 aiimage.http-client.* 兜底。 + * 次数重试/退避来自命名空间,为未来接入切点预留;当前不改变 SimilarAsinImageEmbedder 调用点。 + */ +@Component +@RequiredArgsConstructor +public class ImageDownloadHttpConfigResolver { + + private final HttpClientProperties httpClientProperties; + private final SimilarAsinProperties similarAsinProperties; + + private int downloadSeconds() { + return similarAsinProperties.getImageDownloadTimeoutSeconds(); + } + + /** 连接超时:模块 download 秒数优先(现状 5s);未配置回退命名空间。 */ + public long connectTimeoutMillis() { + int module = downloadSeconds(); + return module > 0 ? module * 1_000L : httpClientProperties.effectiveConnectTimeoutMillis(); + } + + /** 读取超时:模块 download 秒数优先(现状 5s);未配置回退命名空间。 */ + public long readTimeoutMillis() { + int module = downloadSeconds(); + return module > 0 ? module * 1_000L : httpClientProperties.effectiveReadTimeoutMillis(); + } + + /** 调用总超时:模块 download×2(现状 10s);未配置回退命名空间。 */ + public long callTimeoutMillis() { + int module = downloadSeconds(); + return module > 0 ? module * 2_000L : httpClientProperties.effectiveCallTimeoutMillis(); + } + + /** 整批图片预取预算(秒转毫秒):模块优先(现状 1800s);未配置回退命名空间 read。 */ + public long prefetchTimeoutMillis() { + int module = similarAsinProperties.getImagePrefetchTimeoutSeconds(); + return module > 0 ? module * 1_000L : httpClientProperties.effectiveReadTimeoutMillis(); + } + + /** 次数重试:来自命名空间(现状图片下载无次数重试,OkHttp 仅连接失败自愈)。 */ + public int maxRetries() { + return httpClientProperties.effectiveMaxRetries(); + } + + public long baseRetryDelayMillis() { + return httpClientProperties.getBaseRetryDelayMillis(); + } +} diff --git a/backend-java/src/test/java/com/nanri/aiimage/config/ImageDownloadHttpConfigResolverTest.java b/backend-java/src/test/java/com/nanri/aiimage/config/ImageDownloadHttpConfigResolverTest.java new file mode 100644 index 00000000..bca8be84 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/config/ImageDownloadHttpConfigResolverTest.java @@ -0,0 +1,100 @@ +package com.nanri.aiimage.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * task-171:图片下载客户端配置接入契约(plan 10,诚实模式)。 + * + * 现网真实基线(已核实,对应 task-166 契约表"图片下载"行): + * - SimilarAsinImageEmbedder 用自建 OkHttpClient(非共享 JDK 池):connect/read/write = + * aiimage.similar-asin.image-download-timeout-seconds(默认 5s),call = 2×download(10s); + * - 整批预取预算 aiimage.similar-asin.image-prefetch-timeout-seconds(默认 1800s); + * - 无显式次数重试(依赖 OkHttp retryOnConnectionFailure + 上游批次重提),图片缓存幂等。 + * + * resolver 语义:模块级现有配置优先(保证接入切点行为不变),命名空间 aiimage.http-client.* + * 兜底;不改动 SimilarAsinImageEmbedder 调用点,仅提供稳定访问面。 + */ +class ImageDownloadHttpConfigResolverTest { + + private HttpClientProperties http; + private SimilarAsinProperties similarAsin; + private ImageDownloadHttpConfigResolver resolver; + + @BeforeEach + void setUp() { + http = new HttpClientProperties(); + similarAsin = new SimilarAsinProperties(); + resolver = new ImageDownloadHttpConfigResolver(http, similarAsin); + } + + @Test + void defaultsMatchCurrentEmbedder() { + // 模块默认 imageDownloadTimeoutSeconds=5 → OkHttp connect/read 5s、call 10s + assertEquals(5_000L, resolver.readTimeoutMillis()); + assertEquals(5_000L, resolver.connectTimeoutMillis()); + assertEquals(10_000L, resolver.callTimeoutMillis()); + } + + @Test + void prefetchBudgetFromModule() { + // 模块默认 imagePrefetchTimeoutSeconds=1800 → 1800s + assertEquals(1_800_000L, resolver.prefetchTimeoutMillis()); + } + + @Test + void moduleWinsOverNamespace() { + // 模块级现有配置优先:命名空间 read 调大不影响图片下载 read(行为不变) + http.setReadTimeoutMillis(120_000); + assertEquals(5_000L, resolver.readTimeoutMillis()); + } + + @Test + void downloadOverrideTakesEffect() { + similarAsin.setImageDownloadTimeoutSeconds(8); + assertEquals(8_000L, resolver.readTimeoutMillis()); + assertEquals(8_000L, resolver.connectTimeoutMillis()); + assertEquals(16_000L, resolver.callTimeoutMillis(), "call = 2×download"); + } + + @Test + void fallsBackToNamespaceWhenModuleUnset() { + // 模块未配置(<=0) → 命名空间(默认 connect 10s/read 60s/call 90s) + similarAsin.setImageDownloadTimeoutSeconds(0); + similarAsin.setImagePrefetchTimeoutSeconds(0); + assertEquals(10_000L, resolver.connectTimeoutMillis()); + assertEquals(60_000L, resolver.readTimeoutMillis()); + assertEquals(90_000L, resolver.callTimeoutMillis()); + assertEquals(60_000L, resolver.prefetchTimeoutMillis()); + } + + @Test + void retryFromNamespaceClamped() { + // 无显式次数重试是现状;次数来自命名空间,供未来接入切点 + assertEquals(3, resolver.maxRetries()); + http.setMaxRetries(99); + assertEquals(10, resolver.maxRetries(), "命名空间重试钳制到 10"); + assertEquals(500L, resolver.baseRetryDelayMillis()); + } + + @Test + void invalidNamespaceTimeoutsClamped() { + similarAsin.setImageDownloadTimeoutSeconds(0); + similarAsin.setImagePrefetchTimeoutSeconds(0); + http.setConnectTimeoutMillis(-1); + assertEquals(1_000L, resolver.connectTimeoutMillis()); + http.setReadTimeoutMillis(999_999_999L); + assertEquals(3_600_000L, resolver.readTimeoutMillis()); + } + + @Test + void resolverWiredAsStableAccessSurface() { + assertTrue(resolver.connectTimeoutMillis() > 0); + assertTrue(resolver.readTimeoutMillis() > 0); + assertTrue(resolver.callTimeoutMillis() > 0); + assertTrue(resolver.maxRetries() >= 0); + } +}