Skip to content

Commit 2c84912

Browse files
committed
perf: optimize Hermes transport and coverage
1 parent 44597dc commit 2c84912

11 files changed

Lines changed: 1049 additions & 81 deletions

‎pom.xml‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@
194194
<configuration>
195195
<skip>false</skip>
196196
<skipTests>false</skipTests>
197-
<argLine>-Xmx1024m -Dfile.encoding=UTF-8</argLine>
197+
<!--suppress UnresolvedMavenProperty -->
198+
<argLine>${argLine} -Xmx1024m -Dfile.encoding=UTF-8</argLine>
198199
<excludedGroups>integration</excludedGroups>
199200
<additionalClasspathElements>
200201
<additionalClasspathElement>${basedir}/target/test-classes</additionalClasspathElement>
@@ -293,7 +294,7 @@
293294
<goal>check</goal>
294295
</goals>
295296
<configuration>
296-
<haltOnFailure>false</haltOnFailure>
297+
<haltOnFailure>true</haltOnFailure>
297298
<rules>
298299
<rule>
299300
<element>BUNDLE</element>
@@ -343,12 +344,12 @@
343344
<profile>
344345
<id>disable-javadoc-doclint</id>
345346
<activation>
346-
<jdk>[1.8,)</jdk>
347+
<jdk>[21,)</jdk>
347348
</activation>
348349
<properties>
349-
<central-maven-publishing-plugin.version>0.11.0</central-maven-publishing-plugin.version>
350350
<additionalparam>-Xdoclint:none</additionalparam>
351351
<maven-jacoco-plugin.version>0.8.15</maven-jacoco-plugin.version>
352+
<maven-central-publishing-plugin.version>0.11.0</maven-central-publishing-plugin.version>
352353
<junit.version>4.13.2</junit.version>
353354
<slf4j.version>2.0.18</slf4j.version>
354355
<lombok.version>1.18.46</lombok.version>
@@ -477,7 +478,7 @@
477478
<!-- 发布插件:将制品发布到 Maven Central Portal -->
478479
<groupId>org.sonatype.central</groupId>
479480
<artifactId>central-publishing-maven-plugin</artifactId>
480-
<version>${central-maven-publishing-plugin.version}</version>
481+
<version>${maven-central-publishing-plugin.version}</version>
481482
<extensions>true</extensions>
482483
<configuration>
483484
<publishingServerId>central</publishingServerId>
@@ -492,24 +493,26 @@
492493

493494
<!-- 版本属性定义:Java 版本、依赖版本与 Maven 插件版本统一管理 -->
494495
<properties>
495-
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
496-
<java.version>21</java.version>
497496
<maven.compiler.release>${java.version}</maven.compiler.release>
498497
<maven.version>3.0</maven.version>
498+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
499+
<java.version>21</java.version>
499500
<slf4j.version>2.0.18</slf4j.version>
500-
<okhttp3.version>4.12.0</okhttp3.version>
501+
<okhttp3.version>5.4.0</okhttp3.version>
501502
<jackson.version>2.22.0</jackson.version>
502503
<junit.version>5.11.4</junit.version>
503504
<lombok.version>1.18.46</lombok.version>
504505
<commons-exec.version>1.6.0</commons-exec.version>
505506
<!-- Maven Plugin versions -->
507+
<maven-central-publishing-plugin.version>0.11.0</maven-central-publishing-plugin.version>
506508
<maven-clean-plugin.version>3.5.0</maven-clean-plugin.version>
507509
<maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>
508510
<maven-dependency-plugin.version>3.8.1</maven-dependency-plugin.version>
509511
<maven-deploy-plugin.version>3.1.4</maven-deploy-plugin.version>
510512
<maven-enforcer-plugin.version>3.6.3</maven-enforcer-plugin.version>
511513
<maven-gpg-plugin.version>3.2.8</maven-gpg-plugin.version>
512514
<maven-install-plugin.version>3.1.4</maven-install-plugin.version>
515+
<maven-jacoco-plugin.version>0.8.15</maven-jacoco-plugin.version>
513516
<maven-jar-plugin.version>3.5.0</maven-jar-plugin.version>
514517
<maven-javadoc-plugin.version>3.11.2</maven-javadoc-plugin.version>
515518
<maven-release-plugin.version>3.3.1</maven-release-plugin.version>
@@ -525,7 +528,7 @@
525528
<!-- For OkHttp -->
526529
<dependency>
527530
<groupId>com.squareup.okhttp3</groupId>
528-
<artifactId>okhttp</artifactId>
531+
<artifactId>okhttp-jvm</artifactId>
529532
<version>${okhttp3.version}</version>
530533
</dependency>
531534
<!-- For OkHttp -->
@@ -574,7 +577,7 @@
574577
<!-- For OkHttp -->
575578
<dependency>
576579
<groupId>com.squareup.okhttp3</groupId>
577-
<artifactId>okhttp</artifactId>
580+
<artifactId>okhttp-jvm</artifactId>
578581
</dependency>
579582
<!-- For OkHttp -->
580583
<dependency>

‎src/main/java/io/github/easy4j/hermes/HermesClient.java‎

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class HermesClient implements AutoCloseable {
2424
private final HermesHttpClient httpClient;
2525
private final HermesSseClient sseClient;
2626
private final HermesCli cli;
27+
private final OkHttpClient ownedHttpClient;
2728

2829
/**
2930
* 使用组合配置构造客户端(推荐方式)。
@@ -32,7 +33,19 @@ public class HermesClient implements AutoCloseable {
3233
* @param config 组合配置,不得为 null
3334
*/
3435
public HermesClient(HermesClientConfig config) {
35-
this(config, new ObjectMapper(), new OkHttpClient());
36+
this(Objects.requireNonNull(config, "config").getHttp(), config.getCli(), new ObjectMapper(),
37+
HermesOkHttpClientFactory.create(config.getHttp()), true);
38+
}
39+
40+
/**
41+
* 使用组合配置和调用方管理的共享 {@link OkHttpClient}。
42+
* <p>适用于直接注入 Spring 容器中由 okhttp3-extension/starter 配置的客户端。</p>
43+
*
44+
* @param config 组合配置
45+
* @param httpClient 外部共享 OkHttpClient
46+
*/
47+
public HermesClient(HermesClientConfig config, OkHttpClient httpClient) {
48+
this(config, new ObjectMapper(), httpClient);
3649
}
3750

3851
/**
@@ -46,7 +59,8 @@ public HermesClient(HermesClientConfig config, ObjectMapper objectMapper, OkHttp
4659
this(Objects.requireNonNull(config, "config").getHttp(),
4760
config.getCli(),
4861
objectMapper,
49-
httpClient);
62+
httpClient,
63+
false);
5064
}
5165

5266
/**
@@ -99,7 +113,19 @@ public HermesClient(HermesCliConfig cliConfig, ObjectMapper objectMapper, OkHttp
99113
* @param cliConfig CLI 配置,不得为 null
100114
*/
101115
public HermesClient(HermesHttpClientConfig httpConfig, HermesCliConfig cliConfig) {
102-
this(httpConfig, cliConfig, new ObjectMapper(), new OkHttpClient());
116+
this(httpConfig, cliConfig, new ObjectMapper(), HermesOkHttpClientFactory.create(httpConfig), true);
117+
}
118+
119+
/**
120+
* 使用 HTTP/CLI 配置和调用方管理的共享 {@link OkHttpClient}。
121+
*
122+
* @param httpConfig HTTP 配置
123+
* @param cliConfig CLI 配置
124+
* @param httpClient 外部共享 OkHttpClient
125+
*/
126+
public HermesClient(HermesHttpClientConfig httpConfig, HermesCliConfig cliConfig,
127+
OkHttpClient httpClient) {
128+
this(httpConfig, cliConfig, new ObjectMapper(), httpClient);
103129
}
104130

105131
/**
@@ -112,10 +138,16 @@ public HermesClient(HermesHttpClientConfig httpConfig, HermesCliConfig cliConfig
112138
*/
113139
public HermesClient(HermesHttpClientConfig httpConfig, HermesCliConfig cliConfig,
114140
ObjectMapper objectMapper, OkHttpClient httpClient) {
141+
this(httpConfig, cliConfig, objectMapper, httpClient, false);
142+
}
143+
144+
private HermesClient(HermesHttpClientConfig httpConfig, HermesCliConfig cliConfig,
145+
ObjectMapper objectMapper, OkHttpClient httpClient, boolean ownsHttpClient) {
115146
Objects.requireNonNull(httpConfig, "httpConfig");
116147
Objects.requireNonNull(cliConfig, "cliConfig");
117148
Objects.requireNonNull(objectMapper, "objectMapper");
118149
Objects.requireNonNull(httpClient, "httpClient");
150+
this.ownedHttpClient = ownsHttpClient ? httpClient : null;
119151
this.config = new HermesClientConfig();
120152
copyHttpConfig(httpConfig);
121153
copyCliConfig(cliConfig);
@@ -171,6 +203,7 @@ public HermesClient(HermesClientConfig config, HermesHttpClient httpClient,
171203
this.httpClient = Objects.requireNonNull(httpClient, "httpClient");
172204
this.sseClient = Objects.requireNonNull(sseClient, "sseClient");
173205
this.cli = Objects.requireNonNull(cli, "cli");
206+
this.ownedHttpClient = null;
174207
}
175208

176209
private void copyHttpConfig(HermesHttpClientConfig src) {
@@ -181,6 +214,13 @@ private void copyHttpConfig(HermesHttpClientConfig src) {
181214
this.config.getHttp().setApiKey(src.getApiKey());
182215
this.config.getHttp().setConnectTimeoutMillis(src.getConnectTimeoutMillis());
183216
this.config.getHttp().setReadTimeoutMillis(src.getReadTimeoutMillis());
217+
this.config.getHttp().setWriteTimeoutMillis(src.getWriteTimeoutMillis());
218+
this.config.getHttp().setCallTimeoutMillis(src.getCallTimeoutMillis());
219+
this.config.getHttp().setMaxIdleConnections(src.getMaxIdleConnections());
220+
this.config.getHttp().setKeepAliveDurationMillis(src.getKeepAliveDurationMillis());
221+
this.config.getHttp().setMaxRequests(src.getMaxRequests());
222+
this.config.getHttp().setMaxRequestsPerHost(src.getMaxRequestsPerHost());
223+
this.config.getHttp().setRetryOnConnectionFailure(src.isRetryOnConnectionFailure());
184224
this.config.getHttp().setVerifySsl(src.isVerifySsl());
185225
this.config.getHttp().setDefaultModel(src.getDefaultModel());
186226
this.config.getHttp().setDefaultInstructions(src.getDefaultInstructions());
@@ -319,10 +359,9 @@ public ChatStreamingResponse chatCompletionStream(ChatRequest request) {
319359
/** Streaming chat completion with Hermes custom headers. */
320360
public ChatStreamingResponse chatCompletionStream(ChatRequest request,
321361
Map<String, String> headers) {
322-
323-
request.setStream(true);
362+
Objects.requireNonNull(request, "request");
324363
ChatStreamingResponse stream = new ChatStreamingResponse();
325-
sseClient.subscribeChat(request, headers, stream::accept, stream::finish, stream::fail);
364+
sseClient.subscribeChat(request.withStream(), headers, stream::accept, stream::finish, stream::fail);
326365
return stream;
327366
}
328367

@@ -392,9 +431,19 @@ public HermesCli cli() {
392431

393432
public HermesClientConfig getConfig() { return config; }
394433

434+
/**
435+
* 返回 HTTP 与 SSE 共用的 OkHttpClient;HTTP 未启用时返回 null。
436+
*
437+
* @return 实际使用的 OkHttpClient
438+
*/
439+
public OkHttpClient getOkHttpClient() {
440+
return httpClient != null ? httpClient.getOkHttpClient() : null;
441+
}
442+
395443
@Override
396444
public void close() {
397445
if (httpClient != null) httpClient.close();
398446
if (sseClient != null) sseClient.close();
447+
HermesOkHttpClientFactory.shutdown(ownedHttpClient);
399448
}
400449
}

‎src/main/java/io/github/easy4j/hermes/HermesHttpClientConfig.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,27 @@ public class HermesHttpClientConfig {
5252
*/
5353
private int readTimeoutMillis = HermesApiConstants.DEFAULT_READ_TIMEOUT_MS;
5454

55+
/** 写入超时(毫秒)。 */
56+
private int writeTimeoutMillis = 10_000;
57+
58+
/** 整个调用超时(毫秒);0 表示不额外限制。 */
59+
private int callTimeoutMillis;
60+
61+
/** 连接池最大空闲连接数。 */
62+
private int maxIdleConnections = 32;
63+
64+
/** 空闲连接保活时间(毫秒)。 */
65+
private long keepAliveDurationMillis = 300_000L;
66+
67+
/** 异步请求最大并发数。 */
68+
private int maxRequests = 128;
69+
70+
/** 单主机异步请求最大并发数。 */
71+
private int maxRequestsPerHost = 64;
72+
73+
/** 遇到失效连接等传输故障时是否允许 OkHttp 自动恢复。 */
74+
private boolean retryOnConnectionFailure = true;
75+
5576
/**
5677
* 是否校验 HTTPS 证书;为 false 时关闭校验(仅建议开发环境)。
5778
*/
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package io.github.easy4j.hermes;
2+
3+
import okhttp3.ConnectionPool;
4+
import okhttp3.Dispatcher;
5+
import okhttp3.OkHttpClient;
6+
7+
import java.util.Objects;
8+
import java.util.concurrent.TimeUnit;
9+
10+
/**
11+
* Hermes 独立运行时使用的高并发 OkHttpClient 工厂。
12+
*
13+
* <p>Spring 容器已经提供共享 {@link OkHttpClient} 时应使用注入构造器,本工厂不会参与。</p>
14+
*/
15+
public final class HermesOkHttpClientFactory {
16+
17+
private HermesOkHttpClientFactory() {
18+
}
19+
20+
/**
21+
* 根据 Hermes HTTP 配置创建客户端。
22+
*
23+
* @param config HTTP 配置
24+
* @return SDK 自主管理的 OkHttpClient
25+
*/
26+
public static OkHttpClient create(HermesHttpClientConfig config) {
27+
Objects.requireNonNull(config, "config");
28+
Dispatcher dispatcher = new Dispatcher();
29+
dispatcher.setMaxRequests(Math.max(1, config.getMaxRequests()));
30+
dispatcher.setMaxRequestsPerHost(Math.max(1, config.getMaxRequestsPerHost()));
31+
ConnectionPool connectionPool = new ConnectionPool(
32+
Math.max(1, config.getMaxIdleConnections()),
33+
Math.max(1L, config.getKeepAliveDurationMillis()),
34+
TimeUnit.MILLISECONDS);
35+
OkHttpClient.Builder builder = new OkHttpClient.Builder()
36+
.dispatcher(dispatcher)
37+
.connectionPool(connectionPool)
38+
.connectTimeout(Math.max(1, config.getConnectTimeoutMillis()), TimeUnit.MILLISECONDS)
39+
.readTimeout(Math.max(0, config.getReadTimeoutMillis()), TimeUnit.MILLISECONDS)
40+
.writeTimeout(Math.max(1, config.getWriteTimeoutMillis()), TimeUnit.MILLISECONDS)
41+
.callTimeout(Math.max(0, config.getCallTimeoutMillis()), TimeUnit.MILLISECONDS)
42+
.retryOnConnectionFailure(config.isRetryOnConnectionFailure());
43+
if (!config.isVerifySsl()) {
44+
builder.hostnameVerifier((hostname, session) -> true);
45+
}
46+
return builder.build();
47+
}
48+
49+
/**
50+
* 释放 SDK 自建客户端资源。
51+
*
52+
* @param client SDK 自建客户端
53+
*/
54+
public static void shutdown(OkHttpClient client) {
55+
if (Objects.isNull(client)) {
56+
return;
57+
}
58+
client.dispatcher().cancelAll();
59+
client.connectionPool().evictAll();
60+
client.dispatcher().executorService().shutdown();
61+
}
62+
}

‎src/main/java/io/github/easy4j/hermes/api/HermesApiConstants.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ private HermesApiConstants() {}
1414
public static final String DEFAULT_SERVER_URL = "http://localhost:8642";
1515
public static final String DEFAULT_MODEL = "hermes-agent";
1616
public static final String DEFAULT_EXECUTABLE = "hermes";
17-
public static final int DEFAULT_CONNECT_TIMEOUT_MS = 15_000;
18-
public static final int DEFAULT_READ_TIMEOUT_MS = 300_000;
17+
public static final int DEFAULT_CONNECT_TIMEOUT_MS = 2_000;
18+
public static final int DEFAULT_READ_TIMEOUT_MS = 120_000;
1919
public static final int DEFAULT_LOCAL_TIMEOUT_SECONDS = 300;
2020
public static final int DEFAULT_PROBE_TIMEOUT_SECONDS = 5;
2121

‎src/main/java/io/github/easy4j/hermes/api/HermesHttpClient.java‎

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.fasterxml.jackson.databind.DeserializationFeature;
55
import com.fasterxml.jackson.databind.ObjectMapper;
66
import io.github.easy4j.hermes.HermesHttpClientConfig;
7+
import io.github.easy4j.hermes.HermesOkHttpClientFactory;
78
import static io.github.easy4j.hermes.api.HermesApiConstants.*;
89
import io.github.easy4j.hermes.api.model.*;
910
import io.github.easy4j.hermes.exception.HermesHttpException;
@@ -12,7 +13,6 @@
1213

1314
import java.io.IOException;
1415
import java.util.*;
15-
import java.util.concurrent.TimeUnit;
1616

1717
/**
1818
* Hermes Server HTTP 客户端,封装 REST API。
@@ -26,26 +26,25 @@ public class HermesHttpClient implements AutoCloseable {
2626
private final HermesHttpClientConfig config;
2727
private final ObjectMapper objectMapper;
2828
private final OkHttpClient httpClient;
29+
private final boolean ownsHttpClient;
2930

3031
public HermesHttpClient(HermesHttpClientConfig config) {
31-
this(config, null, null);
32+
this(config, null, HermesOkHttpClientFactory.create(config), true);
3233
}
3334

3435
public HermesHttpClient(HermesHttpClientConfig config, ObjectMapper objectMapper, OkHttpClient httpClient) {
36+
this(config, objectMapper,
37+
Objects.isNull(httpClient) ? HermesOkHttpClientFactory.create(config) : httpClient,
38+
Objects.isNull(httpClient));
39+
}
40+
41+
private HermesHttpClient(HermesHttpClientConfig config, ObjectMapper objectMapper,
42+
OkHttpClient httpClient, boolean ownsHttpClient) {
3543
this.config = Objects.requireNonNull(config, "config");
3644
this.objectMapper = Objects.isNull(objectMapper) ? new ObjectMapper()
3745
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) : objectMapper;
38-
this.httpClient = Objects.isNull(httpClient) ? buildOkHttpClient(config) : httpClient;
39-
}
40-
41-
private static OkHttpClient buildOkHttpClient(HermesHttpClientConfig config) {
42-
OkHttpClient.Builder builder = new OkHttpClient.Builder()
43-
.connectTimeout(config.getConnectTimeoutMillis(), TimeUnit.MILLISECONDS)
44-
.readTimeout(config.getReadTimeoutMillis(), TimeUnit.MILLISECONDS);
45-
if (!config.isVerifySsl()) {
46-
builder.hostnameVerifier((hostname, session) -> true);
47-
}
48-
return builder.build();
46+
this.httpClient = Objects.requireNonNull(httpClient, "httpClient");
47+
this.ownsHttpClient = ownsHttpClient;
4948
}
5049

5150
// ============================================================
@@ -393,6 +392,8 @@ private String toJson(Object body) {
393392

394393
@Override
395394
public void close() {
396-
// 外部传入的 OkHttpClient 不关闭;自建的也不主动关闭(OkHttpClient 内部管理连接池)
395+
if (ownsHttpClient) {
396+
HermesOkHttpClientFactory.shutdown(httpClient);
397+
}
397398
}
398399
}

0 commit comments

Comments
 (0)