From 1237d42119261ed1a7522c596969f08dbbb2d585 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:41:58 +0800 Subject: [PATCH] Webclient fix (#173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../skillhub/config/SkillScannerConfig.java | 14 ++++++- .../skill/service/SkillPublishService.java | 2 +- .../service/SkillPublishServiceTest.java | 5 ++- .../skillhub/infra/http/WebClientConfig.java | 37 ++++++++++--------- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillScannerConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillScannerConfig.java index 788e2485..d6b7a5ec 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillScannerConfig.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/SkillScannerConfig.java @@ -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); } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java index 3b7b3720..1355fc2d 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java @@ -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( diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java index 2fcc1b8a..8bc06d70 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java @@ -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 diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/http/WebClientConfig.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/http/WebClientConfig.java index d12befb1..6092e39c 100644 --- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/http/WebClientConfig.java +++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/http/WebClientConfig.java @@ -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); + }; } }