diff --git a/README.md b/README.md index e42cca9..ae4379d 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,12 @@ The following options are available for the builder: - `username` - proxy username - `password` - proxy password - `connectionTimeout` - connection timeout in milliseconds (default: 5000 ms) +- `maxIdleTime` - ConnectionProvider max idle time. (default: no max idle time) +- `maxLifeTime` - ConnectionProvider max life time. (default: no max life time) +- `keepAliveEnabled` - Keep-Alive probe feature flag (default: false) +- `keepAliveIdle` - Keep-Alive idle time +- `keepAliveInterval` - Keep-Alive retransmission interval time +- `keepAliveCount` - Keep-Alive retransmission limit - `acceptInvalidSslCertificate` - whether invalid SSL certificate is accepted (default: false) - `maxInMemorySize` - maximum in memory request size (default: 1048576 bytes) - `httpBasicAuth` - HTTP basic authentication (default: disabled) diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java index 7b3c352..dcae25d 100644 --- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java +++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java @@ -23,8 +23,11 @@ import io.getlime.core.rest.model.base.response.ObjectResponse; import io.getlime.core.rest.model.base.response.Response; import io.netty.channel.ChannelOption; +import io.netty.channel.epoll.EpollChannelOption; +import io.netty.channel.socket.nio.NioChannelOption; import io.netty.handler.logging.LogLevel; import io.netty.handler.ssl.SslContext; +import jdk.net.ExtendedSocketOptions; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +47,7 @@ import org.springframework.web.reactive.function.client.*; import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; import reactor.netty.tcp.SslProvider; import reactor.netty.transport.ProxyProvider; import reactor.netty.transport.logging.AdvancedByteBufFormat; @@ -114,7 +118,7 @@ private void initializeWebClient() throws RestClientException { } final WebClient.Builder builder = WebClient.builder(); final SslContext sslContext = SslUtils.prepareSslContext(config); - HttpClient httpClient = HttpClient.create() + HttpClient httpClient = createHttpClient(config) .wiretap(this.getClass().getCanonicalName(), LogLevel.TRACE, AdvancedByteBufFormat.TEXTUAL) .followRedirect(config.isFollowRedirectEnabled()); if (sslContext != null) { @@ -132,6 +136,10 @@ private void initializeWebClient() throws RestClientException { ChannelOption.CONNECT_TIMEOUT_MILLIS, config.getConnectionTimeout()); } + if (config.isKeepAliveEnabled()) { + httpClient = configureKeepAlive(httpClient, config); + } + final Duration responseTimeout = config.getResponseTimeout(); if (responseTimeout != null) { logger.debug("Setting response timeout {}", responseTimeout); @@ -180,6 +188,50 @@ private void initializeWebClient() throws RestClientException { webClient = builder.baseUrl(config.getBaseUrl()).clientConnector(connector).build(); } + private static HttpClient configureKeepAlive(final HttpClient httpClient, final RestClientConfiguration config) throws RestClientException { + final Duration keepAliveIdle = config.getKeepAliveIdle(); + final Duration keepAliveInterval = config.getKeepAliveInterval(); + final Integer keepAliveCount = config.getKeepAliveCount(); + logger.info("Configuring Keep-Alive, idle={}, interval={}, count={}", keepAliveIdle, keepAliveInterval, keepAliveCount); + if (keepAliveIdle == null || keepAliveInterval == null || keepAliveCount == null) { + throw new RestClientException("All Keep-Alive properties must be specified."); + } + + final int keepIdleSeconds = Math.toIntExact(keepAliveIdle.toSeconds()); + final int keepIntervalSeconds = Math.toIntExact(keepAliveInterval.toSeconds()); + + return httpClient.option(ChannelOption.SO_KEEPALIVE, true) + .option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPIDLE), keepIdleSeconds) + .option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPINTERVAL), keepIntervalSeconds) + .option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPCOUNT), keepAliveCount) + .option(EpollChannelOption.TCP_KEEPIDLE, keepIdleSeconds) + .option(EpollChannelOption.TCP_KEEPINTVL, keepIntervalSeconds) + .option(EpollChannelOption.TCP_KEEPCNT, keepAliveCount); + } + + /** + * Create HttpClient with default HttpConnectionProvider or custom one, if specified in the given config. + * @param config Config to create connection provider if specified. + * @return Http client. + */ + private static HttpClient createHttpClient(final RestClientConfiguration config) { + final Duration maxIdleTime = config.getMaxIdleTime(); + final Duration maxLifeTime = config.getMaxLifeTime(); + if (maxIdleTime != null || maxLifeTime != null) { + logger.info("Configuring custom connection provider, maxIdleTime={}, maxLifeTime={}", maxIdleTime, maxLifeTime); + final ConnectionProvider.Builder providerBuilder = ConnectionProvider.builder("custom"); + if (maxIdleTime != null) { + providerBuilder.maxIdleTime(maxIdleTime); + } + if (maxLifeTime != null) { + providerBuilder.maxLifeTime(maxLifeTime); + } + return HttpClient.create(providerBuilder.build()); + } else { + return HttpClient.create(); + } + } + @Override public ResponseEntity get(String path, ParameterizedTypeReference responseType) throws RestClientException { return get(path, null, null, responseType); @@ -801,6 +853,66 @@ public Builder connectionTimeout(Integer connectionTimeout) { return this; } + /** + * Configure ConnectionProvider max idle time. {@code Null} means no max idle time. + * @param maxIdleTime Max idle time. + * @return Builder. + */ + public Builder maxIdleTime(final Duration maxIdleTime) { + config.setMaxIdleTime(maxIdleTime); + return this; + } + + /** + * Configure ConnectionProvider max life time. {@code Null} means no max life time. + * @param maxLifeTime Max life time. + * @return Builder. + */ + public Builder maxLifeTime(Duration maxLifeTime) { + config.setMaxLifeTime(maxLifeTime); + return this; + } + + /** + * Configure Keep-Alive probe. + * @param keepAliveEnabled Keep-Alive enabled. + * @return Builder. + */ + public Builder keepAliveEnabled(boolean keepAliveEnabled) { + config.setKeepAliveEnabled(keepAliveEnabled); + return this; + } + + /** + * Configure Keep-Alive idle interval. + * @param keepAliveIdle Keep-Alive idle interval. + * @return Builder. + */ + public Builder keepAliveIdle(Duration keepAliveIdle) { + config.setKeepAliveIdle(keepAliveIdle); + return this; + } + + /** + * Configure Keep-Alive retransmission interval. + * @param keepAliveInterval Keep-Alive retransmission interval. + * @return Builder. + */ + public Builder keepAliveInterval(Duration keepAliveInterval) { + config.setKeepAliveInterval(keepAliveInterval); + return this; + } + + /** + * Configure Keep-Alive retransmission limit. + * @param keepAliveCount Keep-Alive retransmission limit. + * @return Builder. + */ + public Builder keepAliveCount(Integer keepAliveCount) { + config.setKeepAliveCount(keepAliveCount); + return this; + } + /** * Configure whether invalid SSL certificate is accepted. * @param acceptInvalidSslCertificate Whether invalid SSL certificate is accepted. diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java index e965866..64fad68 100644 --- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java +++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java @@ -47,6 +47,14 @@ public class RestClientConfiguration { private Integer connectionTimeout = 5000; private Duration responseTimeout; + private Duration maxIdleTime; + private Duration maxLifeTime; + + private boolean keepAliveEnabled; + private Duration keepAliveIdle; + private Duration keepAliveInterval; + private Integer keepAliveCount; + // TLS certificate settings private boolean acceptInvalidSslCertificate = false; private Duration handshakeTimeout; @@ -235,6 +243,108 @@ public void setConnectionTimeout(Integer connectionTimeout) { this.connectionTimeout = connectionTimeout; } + /** + * Get max idle time. + * + * @return Max idle time. + */ + public Duration getMaxIdleTime() { + return maxIdleTime; + } + + /** + * Set the options to use for configuring ConnectionProvider max idle time. + * {@code Null} means no max idle time. + * + * @param maxIdleTime Max idle time. + */ + public void setMaxIdleTime(Duration maxIdleTime) { + this.maxIdleTime = maxIdleTime; + } + + /** + * Get max life time. + * + * @return Max life time. + */ + public Duration getMaxLifeTime() { + return maxLifeTime; + } + + /** + * Set the options to use for configuring ConnectionProvider max life time. + * {@code Null} means no max life time. + * + * @param maxLifeTime Max life time. + */ + public void setMaxLifeTime(Duration maxLifeTime) { + this.maxLifeTime = maxLifeTime; + } + + /** + * Return whether Keep-Alive is enabled. + * @return {@code True} if keep-alive enabled- + */ + public boolean isKeepAliveEnabled() { + return keepAliveEnabled; + } + + /** + * Set whether Keep-Alive is enabled + * @param keepAliveEnabled Keep-Alive. + */ + public void setKeepAliveEnabled(boolean keepAliveEnabled) { + this.keepAliveEnabled = keepAliveEnabled; + } + + /** + * Get Keep-Alive idle time. + * @return Keep-Alive idle time. + */ + public Duration getKeepAliveIdle() { + return keepAliveIdle; + } + + /** + * Set Keep-Alive idle time. + * @param keepAliveIdle Keep-Alive idle time. + */ + public void setKeepAliveIdle(Duration keepAliveIdle) { + this.keepAliveIdle = keepAliveIdle; + } + + /** + * Get Keep-Alive retransmission interval time. + * @return Keep-Alive retransmission interval time. + */ + public Duration getKeepAliveInterval() { + return keepAliveInterval; + } + + /** + * Set Keep-Alive retransmission interval time. + * @param keepAliveInterval Keep-Alive retransmission interval time. + */ + public void setKeepAliveInterval(Duration keepAliveInterval) { + this.keepAliveInterval = keepAliveInterval; + } + + /** + * Get Keep-Alive retransmission limit. + * @return Keep-Alive retransmission limit. + */ + public Integer getKeepAliveCount() { + return keepAliveCount; + } + + /** + * Set Keep-Alive retransmission limit. + * @param keepAliveCount Keep-Alive retransmission limit. + */ + public void setKeepAliveCount(Integer keepAliveCount) { + this.keepAliveCount = keepAliveCount; + } + /** * Get whether invalid SSL certificate is accepted. * @return Whether invalid SSL certificate is accepted.