diff --git a/httpClients/boot-rest-template/pom.xml b/httpClients/boot-rest-template/pom.xml index 98b3b195d..2ee8f75f3 100644 --- a/httpClients/boot-rest-template/pom.xml +++ b/httpClients/boot-rest-template/pom.xml @@ -50,6 +50,11 @@ org.springframework.boot spring-boot-starter-web + + org.apache.httpcomponents.client5 + httpclient5 + + org.springframework.boot spring-boot-devtools @@ -66,6 +71,7 @@ lombok true + org.springframework.boot spring-boot-starter-data-jpa @@ -99,11 +105,6 @@ spring-boot-testcontainers test - - org.awaitility - awaitility - test - org.testcontainers junit-jupiter @@ -219,7 +220,7 @@ - 1.17.0 + 1.19.2 diff --git a/httpClients/boot-rest-template/src/main/java/com/example/rest/template/config/RestTemplateConfiguration.java b/httpClients/boot-rest-template/src/main/java/com/example/rest/template/config/RestTemplateConfiguration.java index 989a8de94..fd1ce42fb 100644 --- a/httpClients/boot-rest-template/src/main/java/com/example/rest/template/config/RestTemplateConfiguration.java +++ b/httpClients/boot-rest-template/src/main/java/com/example/rest/template/config/RestTemplateConfiguration.java @@ -1,15 +1,129 @@ package com.example.rest.template.config; +import static com.example.rest.template.utils.AppConstants.*; + +import java.time.Duration; +import java.util.Iterator; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.ConnectionKeepAliveStrategy; +import org.apache.hc.client5.http.HttpRoute; +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.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.util.TimeValue; +import org.apache.hc.core5.util.Timeout; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.web.client.RestTemplate; @Configuration(proxyBeanMethods = false) +@EnableScheduling +@Slf4j public class RestTemplateConfiguration { @Bean - RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { - return restTemplateBuilder.build(); + PoolingHttpClientConnectionManager poolingConnectionManager() { + PoolingHttpClientConnectionManager poolingConnectionManager = + new PoolingHttpClientConnectionManager(); + // set a total amount of connections across all HTTP routes + poolingConnectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS); + // set a maximum amount of connections for each HTTP route in pool + poolingConnectionManager.setDefaultMaxPerRoute(MAX_ROUTE_CONNECTIONS); + // increase the amounts of connections if the host is localhost + HttpHost localhost = new HttpHost("http://localhost", 8080); + poolingConnectionManager.setMaxPerRoute( + new HttpRoute(localhost), MAX_LOCALHOST_CONNECTIONS); + return poolingConnectionManager; + } + + @Bean + CloseableHttpClient httpClient( + PoolingHttpClientConnectionManager poolingConnectionManager, + ConnectionKeepAliveStrategy connectionKeepAliveStrategy) { + RequestConfig requestConfig = + RequestConfig.custom() + .setConnectTimeout(Timeout.ofSeconds(CONNECTION_TIMEOUT)) + .setConnectionRequestTimeout(Timeout.ofSeconds(REQUEST_TIMEOUT)) + .setResponseTimeout(Timeout.ofSeconds(SOCKET_TIMEOUT)) + .build(); + + return HttpClients.custom() + .setDefaultRequestConfig(requestConfig) + .setConnectionManager(poolingConnectionManager) + .setKeepAliveStrategy(connectionKeepAliveStrategy) + .setConnectionManagerShared(true) + .build(); + } + + @Bean + ConnectionKeepAliveStrategy connectionKeepAliveStrategy() { + return (httpResponse, httpContext) -> { + Iterator
headerIterator = httpResponse.headerIterator(HttpHeaders.KEEP_ALIVE); + while (headerIterator.hasNext()) { + Header element = headerIterator.next(); + String param = element.getName(); + String value = element.getValue(); + if (value != null && param.equalsIgnoreCase("timeout")) { + return TimeValue.ofSeconds(Long.parseLong(value)); + } + } + + return TimeValue.ofSeconds(DEFAULT_KEEP_ALIVE_TIME); + }; + } + + @Bean + RestTemplate restTemplate( + RestTemplateBuilder restTemplateBuilder, CloseableHttpClient httpClient) { + + return restTemplateBuilder + .setConnectTimeout(Duration.ofSeconds(60)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(httpClient)) + .interceptors( + ((request, body, execution) -> { + // log the http request + log.info("URI: {}", request.getURI()); + log.info("HTTP Method: {}", request.getMethod().name()); + log.info("HTTP Headers: {}", request.getHeaders()); + + return execution.execute(request, body); + })) + .build(); + } + + // close idleConnections + @Bean + Runnable idleConnectionMonitor(PoolingHttpClientConnectionManager pool) { + return new Runnable() { + @Override + @Scheduled(fixedDelay = 20000) + public void run() { + // only if connection pool is initialised + if (pool != null) { + log.info("cleaning connection pool"); + pool.closeExpired(); + pool.closeIdle(TimeValue.ofSeconds(IDLE_CONNECTION_WAIT_TIME)); + } + } + }; + } + + // Required for Scheduler + @Bean + TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("idleMonitor"); + scheduler.setPoolSize(5); + return scheduler; } } diff --git a/httpClients/boot-rest-template/src/main/java/com/example/rest/template/utils/AppConstants.java b/httpClients/boot-rest-template/src/main/java/com/example/rest/template/utils/AppConstants.java index c24d8060c..be0b1a7e7 100644 --- a/httpClients/boot-rest-template/src/main/java/com/example/rest/template/utils/AppConstants.java +++ b/httpClients/boot-rest-template/src/main/java/com/example/rest/template/utils/AppConstants.java @@ -1,5 +1,8 @@ package com.example.rest.template.utils; +import lombok.experimental.UtilityClass; + +@UtilityClass public final class AppConstants { public static final String PROFILE_PROD = "prod"; public static final String PROFILE_NOT_PROD = "!" + PROFILE_PROD; @@ -9,4 +12,22 @@ public final class AppConstants { public static final String DEFAULT_PAGE_SIZE = "10"; public static final String DEFAULT_SORT_BY = "id"; public static final String DEFAULT_SORT_DIRECTION = "asc"; + + // Connection pool + public static final int MAX_ROUTE_CONNECTIONS = 40; + public static final int MAX_TOTAL_CONNECTIONS = 40; + public static final int MAX_LOCALHOST_CONNECTIONS = 80; + + // Keep alive + public static final int DEFAULT_KEEP_ALIVE_TIME = 20; // 20 sec + + // Timeouts + public static final int CONNECTION_TIMEOUT = + 30; // 30 sec, the time for waiting until a connection is established + public static final int REQUEST_TIMEOUT = + 30; // 30 sec, the time for waiting for a connection from connection pool + public static final int SOCKET_TIMEOUT = 60; // 60 sec, the time for waiting for data + + // Idle connection monitor + public static final int IDLE_CONNECTION_WAIT_TIME = 30; // 30 sec } diff --git a/httpClients/boot-rest-template/src/test/java/com/example/rest/template/TestApplication.java b/httpClients/boot-rest-template/src/test/java/com/example/rest/template/TestApplication.java index 2a7f461e7..3b3f33c0e 100644 --- a/httpClients/boot-rest-template/src/test/java/com/example/rest/template/TestApplication.java +++ b/httpClients/boot-rest-template/src/test/java/com/example/rest/template/TestApplication.java @@ -11,8 +11,8 @@ public class TestApplication { @ServiceConnection @Bean - PostgreSQLContainer sqlContainer() { - return new PostgreSQLContainer<>("postgres:16.0-alpine"); + PostgreSQLContainer postgreSQLContainer() { + return new PostgreSQLContainer<>("postgres:16.2-alpine"); } public static void main(String[] args) {