task-167: aiimage.http-client.* 配置命名空间(connect/read/call 超时与重试默认值同现状、非法值钳制、env 覆盖)+ 8 条测试;surefire 加 MaxMetaspaceSize 修复 fork 内存崩溃

This commit is contained in:
2026-09-02 08:00:19 +08:00
parent f2736729fa
commit 8ca5b1d53b
4 changed files with 153 additions and 1 deletions
+1 -1
View File
@@ -183,7 +183,7 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<configuration> <configuration>
<argLine>-XX:+EnableDynamicAgentLoading -Xshare:off -Xmx1536m</argLine> <argLine>-XX:+EnableDynamicAgentLoading -Xshare:off -Xmx2g -XX:MaxMetaspaceSize=512m</argLine>
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>
@@ -0,0 +1,59 @@
package com.nanri.aiimage.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 统一 HTTP 客户端配置命名空间(task-167)。
*
* `aiimage.http-client.*`connect/read/call 超时与重试默认值与现状一致
* (RustFS 客户端契约表基线);非法值钳制到合理范围;env 覆盖走宽松绑定。
*/
@Data
@Component
@ConfigurationProperties(prefix = "aiimage.http-client")
public class HttpClientProperties {
/** 连接超时(毫秒),默认 10000 与现状一致。 */
private long connectTimeoutMillis = 10_000;
/** 读取超时(毫秒),默认 60000 与现状一致。 */
private long readTimeoutMillis = 60_000;
/** 调用总超时(毫秒),默认 90000 与现状一致。 */
private long callTimeoutMillis = 90_000;
/** 最大重试次数(0-10),默认 3 与现状一致。 */
private int maxRetries = 3;
/** 重试基础延迟(毫秒),默认 500 与现状一致。 */
private long baseRetryDelayMillis = 500;
/** 钳制后的连接超时:1s-300s。 */
public long effectiveConnectTimeoutMillis() {
return clamp(connectTimeoutMillis, 1_000, 300_000);
}
/** 钳制后的读取超时:1s-3600s。 */
public long effectiveReadTimeoutMillis() {
return clamp(readTimeoutMillis, 1_000, 3_600_000);
}
/** 钳制后的调用总超时:1s-7200s。 */
public long effectiveCallTimeoutMillis() {
return clamp(callTimeoutMillis, 1_000, 7_200_000);
}
/** 钳制后的重试次数:0-10。 */
public int effectiveMaxRetries() {
return (int) clamp(maxRetries, 0, 10);
}
private static long clamp(long value, long min, long max) {
if (value < min) {
return min;
}
return Math.min(value, max);
}
}
@@ -81,6 +81,14 @@ knife4j:
language: zh_cn language: zh_cn
aiimage: aiimage:
# ===== HTTP 客户端统一配置命名空间(task-167)=====
# 默认值与现状一致(RustFS 客户端契约表基线);非法值由 HttpClientProperties 钳制。
http-client:
connect-timeout-millis: ${AIIMAGE_HTTP_CLIENT_CONNECT_TIMEOUT_MILLIS:10000}
read-timeout-millis: ${AIIMAGE_HTTP_CLIENT_READ_TIMEOUT_MILLIS:60000}
call-timeout-millis: ${AIIMAGE_HTTP_CLIENT_CALL_TIMEOUT_MILLIS:90000}
max-retries: ${AIIMAGE_HTTP_CLIENT_MAX_RETRIES:3}
base-retry-delay-millis: ${AIIMAGE_HTTP_CLIENT_BASE_RETRY_DELAY_MILLIS:500}
# Set a stable server-level ID in 1Panel/Docker with AIIMAGE_INSTANCE_ID. # Set a stable server-level ID in 1Panel/Docker with AIIMAGE_INSTANCE_ID.
# For IDEA/local startup, set -Daiimage.instance-id=<stable-id> or configure the AIIMAGE_INSTANCE_ID env var. # For IDEA/local startup, set -Daiimage.instance-id=<stable-id> or configure the AIIMAGE_INSTANCE_ID env var.
# Avoid relying on container hostname/container ID, otherwise task owner continuity may break after restart/redeploy. # Avoid relying on container hostname/container ID, otherwise task owner continuity may break after restart/redeploy.
@@ -0,0 +1,85 @@
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-167aiimage.http-client.* 配置命名空间契约(plan 10)。
* 默认值与现状一致;非法值钳制;配置类绑定字段齐全。
*/
class HttpClientPropertiesTest {
private HttpClientProperties properties;
@BeforeEach
void setUp() {
properties = new HttpClientProperties();
}
@Test
void namespaceDefaultsMatchCurrent() {
assertEquals(10_000L, properties.getConnectTimeoutMillis(), "connect 默认 10s(与现状一致)");
assertEquals(60_000L, properties.getReadTimeoutMillis(), "read 默认 60s");
assertEquals(90_000L, properties.getCallTimeoutMillis(), "call 默认 90s");
assertEquals(3, properties.getMaxRetries(), "重试默认 3");
assertEquals(500L, properties.getBaseRetryDelayMillis(), "退避基础延迟 500ms");
}
@Test
void connectTimeoutBounded() {
assertEquals(10_000L, properties.effectiveConnectTimeoutMillis());
properties.setConnectTimeoutMillis(1_000);
assertEquals(1_000L, properties.effectiveConnectTimeoutMillis());
}
@Test
void readTimeoutBounded() {
assertEquals(60_000L, properties.effectiveReadTimeoutMillis());
properties.setReadTimeoutMillis(3_600_000);
assertEquals(3_600_000L, properties.effectiveReadTimeoutMillis());
}
@Test
void retryCountBounded() {
assertEquals(3, properties.effectiveMaxRetries());
properties.setMaxRetries(0);
assertEquals(0, properties.effectiveMaxRetries());
}
@Test
void invalidValuesClamped() {
properties.setConnectTimeoutMillis(-5);
assertEquals(1_000L, properties.effectiveConnectTimeoutMillis(), "负数钳制到下限");
properties.setReadTimeoutMillis(999_999_999L);
assertEquals(3_600_000L, properties.effectiveReadTimeoutMillis(), "超上限钳制");
properties.setMaxRetries(99);
assertEquals(10, properties.effectiveMaxRetries(), "重试钳制到 10");
}
@Test
void callTimeoutBounded() {
properties.setCallTimeoutMillis(7_200_000);
assertEquals(7_200_000L, properties.effectiveCallTimeoutMillis());
properties.setCallTimeoutMillis(0);
assertEquals(1_000L, properties.effectiveCallTimeoutMillis());
}
@Test
void configClassBindingPrefix() throws Exception {
var annotation = HttpClientProperties.class
.getAnnotation(org.springframework.boot.context.properties.ConfigurationProperties.class);
assertEquals("aiimage.http-client", annotation.prefix(), "绑定 aiimage.http-client 前缀");
assertTrue(HttpClientProperties.class.isAnnotationPresent(
org.springframework.stereotype.Component.class), "配置类需注册为组件");
}
@Test
void envOverrideSemantics() {
// env 覆盖走宽松绑定:设置后 effective 反映新值
properties.setConnectTimeoutMillis(30_000);
assertEquals(30_000L, properties.effectiveConnectTimeoutMillis());
}
}