diff --git a/backend-java/pom.xml b/backend-java/pom.xml index a57f4908..a4e1ee38 100644 --- a/backend-java/pom.xml +++ b/backend-java/pom.xml @@ -183,7 +183,7 @@ org.apache.maven.plugins maven-surefire-plugin - -XX:+EnableDynamicAgentLoading -Xshare:off -Xmx1536m + -XX:+EnableDynamicAgentLoading -Xshare:off -Xmx2g -XX:MaxMetaspaceSize=512m diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/HttpClientProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/HttpClientProperties.java new file mode 100644 index 00000000..7191068e --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/config/HttpClientProperties.java @@ -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); + } +} diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index 2667c03f..5b105c93 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -81,6 +81,14 @@ knife4j: language: zh_cn 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. # For IDEA/local startup, set -Daiimage.instance-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. diff --git a/backend-java/src/test/java/com/nanri/aiimage/config/HttpClientPropertiesTest.java b/backend-java/src/test/java/com/nanri/aiimage/config/HttpClientPropertiesTest.java new file mode 100644 index 00000000..79efe7cd --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/config/HttpClientPropertiesTest.java @@ -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-167:aiimage.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()); + } +}