Webclient fix (#173)

* fix(scanner): wire timeout config and create dedicated scanner HttpClient

- Create scanner-specific HttpClient bean with proper timeout configuration
- Wire SkillScannerProperties.connectTimeoutMs (5s) and readTimeoutMs (5min) into WebClient
- Add logging for timeout configuration verification
- Fix cloud environment 5-second timeout issue by ensuring responseTimeout is properly applied

The previous global WebClient.Builder configuration was not being used correctly,
causing scanner requests to timeout after 5 seconds in cloud environment.
This fix creates a dedicated HttpClient for scanner with explicit timeout settings.

* refactor(webclient): use WebClientCustomizer and add scanner connection pool

- Replace singleton WebClient.Builder bean with WebClientCustomizer
  (follows Spring Boot best practice for prototype-scoped builder)
- Add connection pool config to scanner HttpClient (maxConn=10,
  maxIdleTime=20s, evictInBackground=30s) to prevent stale connections
- Add connectTimeout to global WebClient config

* fix(skill): use system default timezone for auto-generated version numbers

- Change AUTO_VERSION_FORMATTER from UTC to ZoneId.systemDefault()
- Version format yyyyMMdd.HHmmss now uses server's local timezone
- Update test to validate format instead of exact value (timezone-independent)

This allows the service to adapt to deployment location:
- Deployed in China → uses Asia/Shanghai timezone
- Deployed in US → uses US timezone
- Follows server's system timezone configuration
This commit is contained in:
XiaoSeS 2026-03-27 15:41:58 +08:00 committed by GitHub
parent fb9b11e750
commit 1237d42119
4 changed files with 37 additions and 21 deletions

View file

@ -33,7 +33,17 @@ public class SkillScannerConfig {
log.info("Creating scanner-specific HttpClient with connectTimeout={}ms, readTimeout={}ms",
connectTimeoutMs, readTimeoutMs);
reactor.netty.http.client.HttpClient reactorClient = reactor.netty.http.client.HttpClient.create()
// Configure connection pool to avoid stale connections and improve reliability
reactor.netty.resources.ConnectionProvider connectionProvider =
reactor.netty.resources.ConnectionProvider.builder("scanner-pool")
.maxConnections(10)
.maxIdleTime(Duration.ofSeconds(20))
.maxLifeTime(Duration.ofSeconds(60))
.pendingAcquireTimeout(Duration.ofSeconds(45))
.evictInBackground(Duration.ofSeconds(30))
.build();
reactor.netty.http.client.HttpClient reactorClient = reactor.netty.http.client.HttpClient.create(connectionProvider)
.followRedirect(false)
.responseTimeout(Duration.ofMillis(readTimeoutMs))
.option(io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMs);
@ -47,7 +57,7 @@ public class SkillScannerConfig {
.exchangeStrategies(strategies)
.build();
log.info("Scanner HttpClient created successfully with responseTimeout={}ms", readTimeoutMs);
log.info("Scanner HttpClient created with connection pool (maxConn=10, maxIdleTime=20s, evictInterval=30s)");
return new WebClientHttpClient(webClient);
}

View file

@ -61,7 +61,7 @@ import java.util.zip.ZipOutputStream;
public class SkillPublishService {
private static final DateTimeFormatter AUTO_VERSION_FORMATTER =
DateTimeFormatter.ofPattern("yyyyMMdd.HHmmss").withZone(ZoneId.of("Asia/Shanghai"));
DateTimeFormatter.ofPattern("yyyyMMdd.HHmmss").withZone(ZoneId.systemDefault());
private static final Logger log = LoggerFactory.getLogger(SkillPublishService.class);
public record PublishResult(

View file

@ -567,7 +567,10 @@ class SkillPublishServiceTest {
SkillPublishService.PublishResult result = service.publishFromEntries(
namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC, Set.of());
assertEquals("20260318.200000", result.version().getVersion());
// Version should be auto-generated in format yyyyMMdd.HHmmss using system timezone
String version = result.version().getVersion();
assertTrue(version.matches("\\d{8}\\.\\d{6}"), "Version should match format yyyyMMdd.HHmmss");
assertTrue(version.startsWith("20260318"), "Version should start with date 20260318");
}
@Test

View file

@ -1,33 +1,36 @@
package com.iflytek.skillhub.infra.http;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import java.time.Duration;
@Configuration
public class WebClientConfig {
@Bean
public WebClient.Builder webClientBuilder() {
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024))
.build();
reactor.netty.http.client.HttpClient reactorClient = reactor.netty.http.client.HttpClient.create()
.followRedirect(false)
.responseTimeout(Duration.ofMinutes(5));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(reactorClient))
.exchangeStrategies(strategies);
}
private static final Logger log = LoggerFactory.getLogger(WebClientConfig.class);
@Bean
public HttpClient httpClient(WebClient.Builder webClientBuilder) {
return new WebClientHttpClient(webClientBuilder.build());
public WebClientCustomizer globalWebClientCustomizer() {
log.info("Registering global WebClientCustomizer with 5-minute responseTimeout and 10-second connectTimeout");
return builder -> {
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024))
.build();
reactor.netty.http.client.HttpClient reactorClient = reactor.netty.http.client.HttpClient.create()
.followRedirect(false)
.responseTimeout(Duration.ofMinutes(5))
.option(io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
builder.clientConnector(new ReactorClientHttpConnector(reactorClient))
.exchangeStrategies(strategies);
};
}
}