task-171: 图片下载客户端配置接入(ImageDownloadHttpConfigResolver,download/prefetch 模块级现有配置优先、命名空间兜底)+ 8 条测试

- 真实基线:SimilarAsinImageEmbedder(OkHttp) connect/read=imageDownloadTimeoutSeconds(5s)、call=2×download(10s)、prefetch 1800s
- resolver 提供 connect/read/call/prefetch/maxRetries 访问面,模块级优先、命名空间兜底,不改调用点
- 仅新增类与测试,零生产行为变化
This commit is contained in:
2026-09-04 23:10:36 +08:00
parent ec23c0d77c
commit c1f03d64d1
2 changed files with 161 additions and 0 deletions
@@ -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();
}
}
@@ -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×download10s);
* - 整批预取预算 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);
}
}