diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index ea7e4f7d3..edb60cde7 100755 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -112,6 +112,19 @@ NOTE: Because of a limitation in Eureka, it is not possible to support per-serve If you want to customize the RestTemplate used by the Eureka HTTP Client you may want to create a bean of `EurekaClientHttpRequestFactorySupplier` and provide your own logic for generating a `ClientHttpRequestFactory` instance. +All default timeout-related properties for RestTemplate used by the Eureka HTTP Client are set to 3 minutes (in keeping with Apache HC5 default `RequestConfig` and `SocketConfig`). Therefore, to specify the timeout values, you must specify the value directly with the properties in `eureka.client.rest-template-timeout`. (All timeout properties are in milliseconds.) + +.application.yml +[source,yaml] +---- +eureka: + client: + rest-template-timeout: + connect-timeout: 5000 + connect-request-timeout: 8000 + socket-timeout: 10000 +---- + === Status Page and Health Indicator The status page and health indicators for a Eureka instance default to `/info` and `/health` respectively, which are the default locations of useful endpoints in a Spring Boot Actuator application. diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/RestTemplateTimeoutProperties.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/RestTemplateTimeoutProperties.java new file mode 100644 index 000000000..394420ddb --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/RestTemplateTimeoutProperties.java @@ -0,0 +1,98 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.netflix.eureka; + +import java.util.Objects; + +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.http.io.SocketConfig; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.netflix.eureka.http.RestTemplateEurekaHttpClient; +import org.springframework.web.client.RestTemplate; + +/** + * Properties for configuring timeouts used in {@link RestTemplate} required by + * {@link RestTemplateEurekaHttpClient}. + * + * @author Jiwon Jeon + * @since 3.1.6 + */ +@ConfigurationProperties("eureka.client.rest-template-timeout") +public class RestTemplateTimeoutProperties { + + /** + * Default values are set to 180000, in keeping with {@link RequestConfig} and + * {@link SocketConfig} defaults. + */ + private int connectTimeout = 3 * 60 * 1000; + + private int connectRequestTimeout = 3 * 60 * 1000; + + private int socketTimeout = 3 * 60 * 1000; + + public int getConnectTimeout() { + return connectTimeout; + } + + public int getConnectRequestTimeout() { + return connectRequestTimeout; + } + + public int getSocketTimeout() { + return socketTimeout; + } + + public void setConnectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public void setConnectRequestTimeout(int connectRequestTimeout) { + this.connectRequestTimeout = connectRequestTimeout; + } + + public void setSocketTimeout(int socketTimeout) { + this.socketTimeout = socketTimeout; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + RestTemplateTimeoutProperties that = (RestTemplateTimeoutProperties) o; + + return connectTimeout == that.connectTimeout && connectRequestTimeout == that.connectRequestTimeout + && socketTimeout == that.socketTimeout; + } + + @Override + public int hashCode() { + return Objects.hash(connectTimeout, connectRequestTimeout, socketTimeout); + } + + @Override + public String toString() { + return "RestTemplateTimeoutProperties{" + ", connectTimeout=" + connectTimeout + ", connectRequestTimeout=" + + connectRequestTimeout + ", socketTimeout=" + socketTimeout + '}'; + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java index 8cc616a45..be5e185ac 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/DiscoveryClientOptionalArgsConfiguration.java @@ -32,8 +32,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClas import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.configuration.SSLContextFactory; import org.springframework.cloud.configuration.TlsProperties; +import org.springframework.cloud.netflix.eureka.RestTemplateTimeoutProperties; import org.springframework.cloud.netflix.eureka.http.DefaultEurekaClientHttpRequestFactorySupplier; import org.springframework.cloud.netflix.eureka.http.EurekaClientHttpRequestFactorySupplier; import org.springframework.cloud.netflix.eureka.http.RestTemplateDiscoveryClientOptionalArgs; @@ -48,6 +50,7 @@ import org.springframework.web.reactive.function.client.WebClient; * @author Daniel Lavoie */ @Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(RestTemplateTimeoutProperties.class) public class DiscoveryClientOptionalArgsConfiguration { protected static final Log logger = LogFactory.getLog(DiscoveryClientOptionalArgsConfiguration.class); @@ -88,8 +91,9 @@ public class DiscoveryClientOptionalArgsConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnClass(name = "org.springframework.web.client.RestTemplate") - EurekaClientHttpRequestFactorySupplier defaultEurekaClientHttpRequestFactorySupplier() { - return new DefaultEurekaClientHttpRequestFactorySupplier(); + EurekaClientHttpRequestFactorySupplier defaultEurekaClientHttpRequestFactorySupplier( + RestTemplateTimeoutProperties restTemplateTimeoutProperties) { + return new DefaultEurekaClientHttpRequestFactorySupplier(restTemplateTimeoutProperties); } // FIXME: 4.0 diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/DefaultEurekaClientHttpRequestFactorySupplier.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/DefaultEurekaClientHttpRequestFactorySupplier.java index d6075995a..36bf1b35a 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/DefaultEurekaClientHttpRequestFactorySupplier.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/http/DefaultEurekaClientHttpRequestFactorySupplier.java @@ -16,16 +16,22 @@ package org.springframework.cloud.netflix.eureka.http; +import java.util.concurrent.TimeUnit; + import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; +import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.io.HttpClientConnectionManager; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; +import org.apache.hc.core5.http.io.SocketConfig; +import org.apache.hc.core5.util.Timeout; +import org.springframework.cloud.netflix.eureka.RestTemplateTimeoutProperties; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.lang.Nullable; @@ -36,24 +42,47 @@ import org.springframework.lang.Nullable; * * @author Marcin Grzejszczak * @author Olga Maciaszek-Sharma + * @author Jiwon Jeon * @since 3.0.0 */ public class DefaultEurekaClientHttpRequestFactorySupplier implements EurekaClientHttpRequestFactorySupplier { + private final RestTemplateTimeoutProperties restTemplateTimeoutProperties; + + /** + * @deprecated in favour of + * {@link DefaultEurekaClientHttpRequestFactorySupplier#DefaultEurekaClientHttpRequestFactorySupplier(RestTemplateTimeoutProperties)} + */ + @Deprecated + public DefaultEurekaClientHttpRequestFactorySupplier() { + this.restTemplateTimeoutProperties = new RestTemplateTimeoutProperties(); + } + + public DefaultEurekaClientHttpRequestFactorySupplier(RestTemplateTimeoutProperties restTemplateTimeoutProperties) { + this.restTemplateTimeoutProperties = restTemplateTimeoutProperties; + } + @Override public ClientHttpRequestFactory get(SSLContext sslContext, @Nullable HostnameVerifier hostnameVerifier) { HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); - if (sslContext != null || hostnameVerifier != null) { - httpClientBuilder.setConnectionManager(buildConnectionManager(sslContext, hostnameVerifier)); + if (sslContext != null || hostnameVerifier != null || restTemplateTimeoutProperties != null) { + httpClientBuilder.setConnectionManager( + buildConnectionManager(sslContext, hostnameVerifier, restTemplateTimeoutProperties)); } + if (restTemplateTimeoutProperties != null) { + httpClientBuilder.setDefaultRequestConfig(buildRequestConfig()); + } + CloseableHttpClient httpClient = httpClientBuilder.build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(httpClient); return requestFactory; } - private HttpClientConnectionManager buildConnectionManager(SSLContext sslContext, - HostnameVerifier hostnameVerifier) { + private HttpClientConnectionManager buildConnectionManager(SSLContext sslContext, HostnameVerifier hostnameVerifier, + RestTemplateTimeoutProperties restTemplateTimeoutProperties) { + PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder + .create(); SSLConnectionSocketFactoryBuilder sslConnectionSocketFactoryBuilder = SSLConnectionSocketFactoryBuilder .create(); if (sslContext != null) { @@ -62,8 +91,21 @@ public class DefaultEurekaClientHttpRequestFactorySupplier implements EurekaClie if (hostnameVerifier != null) { sslConnectionSocketFactoryBuilder.setHostnameVerifier(hostnameVerifier); } - return PoolingHttpClientConnectionManagerBuilder.create() - .setSSLSocketFactory(sslConnectionSocketFactoryBuilder.build()).build(); + connectionManagerBuilder.setSSLSocketFactory(sslConnectionSocketFactoryBuilder.build()); + if (restTemplateTimeoutProperties != null) { + connectionManagerBuilder.setDefaultSocketConfig(SocketConfig.custom() + .setSoTimeout(Timeout.of(restTemplateTimeoutProperties.getSocketTimeout(), TimeUnit.MILLISECONDS)) + .build()); + } + return connectionManagerBuilder.build(); + } + + private RequestConfig buildRequestConfig() { + return RequestConfig.custom() + .setConnectTimeout(Timeout.of(restTemplateTimeoutProperties.getConnectTimeout(), TimeUnit.MILLISECONDS)) + .setConnectionRequestTimeout( + Timeout.of(restTemplateTimeoutProperties.getConnectRequestTimeout(), TimeUnit.MILLISECONDS)) + .build(); } }