Skip to content

Commit e09c14c

Browse files
authored
feat: Http connection pooling (#22)
* feat: HTTP connection pooling * feat: HTTP connection pooling
1 parent eb6cc66 commit e09c14c

File tree

7 files changed

+180
-20
lines changed

7 files changed

+180
-20
lines changed

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ repositories {
3232
val springDocOpenApiVersion = "2.8.5"
3333
val openApiToolsVersion = "0.2.6"
3434
val micrometerVersion = "1.4.3"
35+
val httpClientVersion = "5.4.2"
3536

3637
dependencies {
3738
implementation("org.springframework.boot:spring-boot-starter")
@@ -43,6 +44,7 @@ dependencies {
4344
implementation("io.micrometer:micrometer-registry-prometheus")
4445
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
4546
implementation("org.openapitools:jackson-databind-nullable:$openApiToolsVersion")
47+
implementation("org.apache.httpcomponents.client5:httpclient5:$httpClientVersion")
4648

4749
compileOnly("org.projectlombok:lombok")
4850
annotationProcessor("org.projectlombok:lombok")

gradle.lockfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ jakarta.annotation:jakarta.annotation-api:2.1.1=compileClasspath
4444
jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath
4545
jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=compileClasspath
4646
org.apache.commons:commons-lang3:3.17.0=compileClasspath
47+
org.apache.httpcomponents.client5:httpclient5:5.4.2=compileClasspath
48+
org.apache.httpcomponents.core5:httpcore5-h2:5.3.3=compileClasspath
49+
org.apache.httpcomponents.core5:httpcore5:5.3.3=compileClasspath
4750
org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath
4851
org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath
4952
org.apache.tomcat.embed:tomcat-embed-core:10.1.36=compileClasspath
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package it.gov.pagopa.template.config;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
import lombok.NoArgsConstructor;
6+
import lombok.experimental.SuperBuilder;
7+
import org.springframework.boot.context.properties.NestedConfigurationProperty;
8+
9+
@Data
10+
@SuperBuilder
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
public class HttpClientConfig {
14+
@NestedConfigurationProperty
15+
private HttpClientConnectionPoolConfig connectionPool;
16+
@NestedConfigurationProperty
17+
private HttpClientTimeoutConfig timeout;
18+
19+
@Data
20+
@SuperBuilder
21+
@NoArgsConstructor
22+
@AllArgsConstructor
23+
public static class HttpClientConnectionPoolConfig {
24+
private int size;
25+
private int sizePerRoute;
26+
private long timeToLiveMinutes;
27+
}
28+
29+
@Data
30+
@SuperBuilder
31+
@NoArgsConstructor
32+
@AllArgsConstructor
33+
public static class HttpClientTimeoutConfig {
34+
private long connectMillis;
35+
private long readMillis;
36+
}
37+
}

src/main/java/it/gov/pagopa/template/config/RestTemplateConfig.java

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package it.gov.pagopa.template.config;
22

33
import it.gov.pagopa.template.performancelogger.RestInvokePerformanceLogger;
4+
import it.gov.pagopa.template.utils.HttpUtils;
45
import it.gov.pagopa.template.utils.SecurityUtils;
56
import jakarta.annotation.Nonnull;
67
import lombok.extern.slf4j.Slf4j;
8+
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
79
import org.slf4j.Logger;
810
import org.slf4j.LoggerFactory;
9-
import org.springframework.beans.factory.annotation.Value;
1011
import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer;
12+
import org.springframework.boot.context.properties.ConfigurationProperties;
13+
import org.springframework.boot.ssl.SslBundles;
1114
import org.springframework.boot.web.client.RestTemplateBuilder;
1215
import org.springframework.context.annotation.Bean;
1316
import org.springframework.context.annotation.Configuration;
@@ -21,28 +24,23 @@
2124

2225
import java.io.IOException;
2326
import java.net.URI;
24-
import java.time.Duration;
2527

2628
@Slf4j
2729
@Configuration(proxyBeanMethods = false)
2830
public class RestTemplateConfig {
29-
private final int connectTimeoutMillis;
30-
private final int readTimeoutHandlerMillis;
3131

32-
public RestTemplateConfig(
33-
@Value("${rest.default-timeout.connect-millis}") int connectTimeoutMillis,
34-
@Value("${rest.default-timeout.read-millis}") int readTimeoutHandlerMillis) {
35-
this.connectTimeoutMillis = connectTimeoutMillis;
36-
this.readTimeoutHandlerMillis = readTimeoutHandlerMillis;
37-
}
32+
@Bean
33+
@ConfigurationProperties(prefix = "rest.defaults")
34+
public HttpClientConfig defaultHttpClientConfig(){
35+
return new HttpClientConfig();
36+
}
3837

39-
@Bean
40-
public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer configurer) {
41-
return configurer.configure(new RestTemplateBuilder())
42-
.additionalInterceptors(new RestInvokePerformanceLogger())
43-
.connectTimeout(Duration.ofMillis(connectTimeoutMillis))
44-
.readTimeout(Duration.ofMillis(readTimeoutHandlerMillis));
45-
}
38+
@Bean
39+
public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer configurer, HttpClientConfig defaultHttpClientConfig, SslBundles sslBundles) {
40+
return configurer.configure(new RestTemplateBuilder())
41+
.additionalInterceptors(new RestInvokePerformanceLogger())
42+
.requestFactoryBuilder(HttpUtils.buildPooledConnection(defaultHttpClientConfig, DefaultClientTlsStrategy.createSystemDefault()));
43+
}
4644

4745
public static ResponseErrorHandler bodyPrinterWhenError(String applicationName) {
4846
final Logger errorBodyLogger = LoggerFactory.getLogger("REST_INVOKE." + applicationName);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package it.gov.pagopa.template.utils;
2+
3+
import it.gov.pagopa.template.config.HttpClientConfig;
4+
import org.apache.hc.client5.http.config.ConnectionConfig;
5+
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
6+
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
7+
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
8+
import org.apache.hc.core5.pool.PoolReusePolicy;
9+
import org.apache.hc.core5.util.TimeValue;
10+
import org.apache.hc.core5.util.Timeout;
11+
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
12+
import org.springframework.boot.http.client.HttpComponentsClientHttpRequestFactoryBuilder;
13+
14+
public class HttpUtils {
15+
private HttpUtils() {
16+
}
17+
18+
public static PoolingHttpClientConnectionManagerBuilder getPooledConnectionManagerBuilder(HttpClientConfig httpClientConfig, TlsSocketStrategy tlsSocketStrategy) {
19+
return PoolingHttpClientConnectionManagerBuilder.create()
20+
.setTlsSocketStrategy(tlsSocketStrategy)
21+
.setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT)
22+
.setConnPoolPolicy(PoolReusePolicy.LIFO)
23+
.setMaxConnPerRoute(httpClientConfig.getConnectionPool().getSizePerRoute())
24+
.setMaxConnTotal(httpClientConfig.getConnectionPool().getSize())
25+
.setDefaultConnectionConfig(ConnectionConfig.custom()
26+
.setSocketTimeout(Timeout.ofMilliseconds(httpClientConfig.getTimeout().getReadMillis()))
27+
.setConnectTimeout(Timeout.ofMilliseconds(httpClientConfig.getTimeout().getConnectMillis()))
28+
.setTimeToLive(TimeValue.ofMinutes(httpClientConfig.getConnectionPool().getTimeToLiveMinutes()))
29+
.build());
30+
}
31+
32+
public static HttpComponentsClientHttpRequestFactoryBuilder buildPooledConnection(HttpClientConfig httpClientConfig, TlsSocketStrategy tlsSocketStrategy) {
33+
return ClientHttpRequestFactoryBuilder.httpComponents()
34+
.withHttpClientCustomizer(configurer -> configurer
35+
.setConnectionManager(getPooledConnectionManagerBuilder(httpClientConfig, tlsSocketStrategy).build()));
36+
}
37+
}

src/main/resources/application.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ logging:
5656
PERFORMANCE_LOG.REST_INVOKE: "\${LOG_LEVEL_PERFORMANCE_LOG_REST_INVOKE:\${logging.level.PERFORMANCE_LOG}}"
5757

5858
rest:
59-
default-timeout:
60-
connect-millis: "\${DEFAULT_REST_CONNECT_TIMEOUT_MILLIS:120000}"
61-
read-millis: "\${DEFAULT_REST_READ_TIMEOUT_MILLIS:120000}"
59+
defaults:
60+
connection-pool:
61+
size: "\${DEFAULT_REST_CONNECTION_POOL_SIZE:10}"
62+
size-per-route: "\${DEFAULT_REST_CONNECTION_POOL_SIZE_PER_ROUTE:5}"
63+
time-to-live-minutes: "\${DEFAULT_REST_CONNECTION_POOL_TIME_TO_LIVE_MINUTES:10}"
64+
timeout:
65+
connect-millis: "\${DEFAULT_REST_TIMEOUT_CONNECT_MILLIS:120000}"
66+
read-millis: "\${DEFAULT_REST_TIMEOUT_READ_MILLIS:120000}"
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package it.gov.pagopa.template.utils;
2+
3+
import it.gov.pagopa.template.config.HttpClientConfig;
4+
import org.apache.hc.client5.http.HttpRoute;
5+
import org.apache.hc.client5.http.classic.HttpClient;
6+
import org.apache.hc.client5.http.config.ConnectionConfig;
7+
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
8+
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
9+
import org.apache.hc.core5.function.Resolver;
10+
import org.apache.hc.core5.util.Timeout;
11+
import org.junit.jupiter.api.Assertions;
12+
import org.junit.jupiter.api.Test;
13+
import org.mockito.Mockito;
14+
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
15+
16+
import java.lang.reflect.Field;
17+
18+
class HttpUtilsTest {
19+
20+
@Test
21+
void whenGetPooledConnectionManagerBuilderThenReturnConfiguredConnectionManager() throws NoSuchFieldException, IllegalAccessException {
22+
// Given
23+
HttpClientConfig httpClientConfig = buildTestHttpClientConfig();
24+
TlsSocketStrategy tlsSocketStrategy = Mockito.mock(TlsSocketStrategy.class);
25+
26+
// When
27+
PoolingHttpClientConnectionManager result = HttpUtils.getPooledConnectionManagerBuilder(httpClientConfig, tlsSocketStrategy).build();
28+
29+
// Then
30+
assertHttpClientConnectionManager(result);
31+
}
32+
33+
@Test
34+
void whenBuildPooledConnectionThenReturnConfiguredConnectionManager() throws NoSuchFieldException, IllegalAccessException {
35+
// Given
36+
HttpClientConfig httpClientConfig = buildTestHttpClientConfig();
37+
TlsSocketStrategy tlsSocketStrategy = Mockito.mock(TlsSocketStrategy.class);
38+
39+
// When
40+
HttpComponentsClientHttpRequestFactory result = HttpUtils.buildPooledConnection(httpClientConfig, tlsSocketStrategy).build();
41+
42+
// Then
43+
HttpClient httpClient = result.getHttpClient();
44+
Field connManagerField = httpClient.getClass().getDeclaredField("connManager");
45+
connManagerField.setAccessible(true);
46+
PoolingHttpClientConnectionManager pooledConnectionManager = (PoolingHttpClientConnectionManager) connManagerField.get(httpClient);
47+
assertHttpClientConnectionManager(pooledConnectionManager);
48+
}
49+
50+
private static HttpClientConfig buildTestHttpClientConfig() {
51+
return HttpClientConfig.builder()
52+
.connectionPool(HttpClientConfig.HttpClientConnectionPoolConfig.builder()
53+
.size(10)
54+
.sizePerRoute(5)
55+
.timeToLiveMinutes(3)
56+
.build())
57+
.timeout(HttpClientConfig.HttpClientTimeoutConfig.builder()
58+
.connectMillis(1000)
59+
.readMillis(3000)
60+
.build())
61+
.build();
62+
}
63+
64+
@SuppressWarnings("unchecked")
65+
private static void assertHttpClientConnectionManager(PoolingHttpClientConnectionManager result) throws NoSuchFieldException, IllegalAccessException {
66+
Assertions.assertEquals(10, result.getMaxTotal());
67+
Assertions.assertEquals(5, result.getDefaultMaxPerRoute());
68+
69+
Field connectionConfigResolverField = PoolingHttpClientConnectionManager.class.getDeclaredField("connectionConfigResolver");
70+
connectionConfigResolverField.setAccessible(true);
71+
Resolver<HttpRoute, ConnectionConfig> connectionConfigResolver = (Resolver<HttpRoute, ConnectionConfig>) connectionConfigResolverField.get(result);
72+
ConnectionConfig connectionConfig = connectionConfigResolver.resolve(null);
73+
74+
Assertions.assertEquals(Timeout.ofMilliseconds(1_000), connectionConfig.getConnectTimeout());
75+
Assertions.assertEquals(Timeout.ofMilliseconds(3_000), connectionConfig.getSocketTimeout());
76+
Assertions.assertEquals(Timeout.ofMilliseconds(180_000), connectionConfig.getTimeToLive());
77+
}
78+
}

0 commit comments

Comments
 (0)