From 66d033e5bb4317d02d343fc3a5f50dde2d4097d6 Mon Sep 17 00:00:00 2001 From: "waqas.ikram" Date: Thu, 14 Dec 2023 12:17:03 +0000 Subject: [PATCH] Enhancing the REST template with HttpClient5 for better performance and allowing users to configure timeouts as per their requirements Issue-ID: CPS-1994 Change-Id: I9fa94fb3923a50e33b3850ec0f190a51e278698f Signed-off-by: waqas.ikram --- cps-ncmp-service/pom.xml | 4 ++ .../api/impl/config/HttpClientConfiguration.java | 57 ++++++++++++++++++++++ .../ncmp/api/impl/config/NcmpConfiguration.java | 48 +++++++++++++++--- ...rkCmProxyDataServiceImplRegistrationSpec.groovy | 2 +- .../impl/config/HttpClientConfigurationSpec.groovy | 48 ++++++++++++++++++ .../api/impl/config/NcmpConfigurationSpec.groovy | 18 +++++-- 6 files changed, 163 insertions(+), 14 deletions(-) create mode 100644 cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy diff --git a/cps-ncmp-service/pom.xml b/cps-ncmp-service/pom.xml index f448c8f47..e9faf4349 100644 --- a/cps-ncmp-service/pom.xml +++ b/cps-ncmp-service/pom.xml @@ -41,6 +41,10 @@ org.apache.commons commons-lang3 + + org.apache.httpcomponents.client5 + httpclient5 + io.cloudevents cloudevents-json-jackson diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java new file mode 100644 index 000000000..aaa4f1e5b --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java @@ -0,0 +1,57 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation. + * ================================================================================ + * 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 + * + * http://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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.config; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DurationUnit; + +@Getter +@Setter +@ConfigurationProperties(prefix = "httpclient5", ignoreUnknownFields = true) +public class HttpClientConfiguration { + + /** + * The maximum time to establish a connection. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration connectionTimeoutInSeconds = Duration.ofSeconds(180); + + /** + * The maximum number of open connections per route. + */ + private int maximumConnectionsPerRoute = 50; + + /** + * The maximum total number of open connections. + */ + private int maximumConnectionsTotal = maximumConnectionsPerRoute * 2; + + /** + * The duration after which idle connections are evicted. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration idleConnectionEvictionThresholdInSeconds = Duration.ofSeconds(5); + +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java index ffecf9c7f..4460094f5 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2022 Nordix Foundation + * Copyright (C) 2021-2023 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,29 +20,36 @@ package org.onap.cps.ncmp.api.impl.config; -import java.time.Duration; import java.util.Arrays; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.apache.hc.client5.http.config.ConnectionConfig; +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.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.core5.util.TimeValue; +import org.apache.hc.core5.util.Timeout; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Configuration +@EnableConfigurationProperties(HttpClientConfiguration.class) @RequiredArgsConstructor(access = AccessLevel.PROTECTED) public class NcmpConfiguration { - private static final Duration CONNECTION_TIMEOUT_MILLISECONDS = Duration.ofMillis(180000); - private static final Duration READ_TIMEOUT_MILLISECONDS = Duration.ofMillis(180000); - @Getter @Component public static class DmiProperties { @@ -60,13 +67,38 @@ public class NcmpConfiguration { * Rest template bean. * * @param restTemplateBuilder the rest template builder + * @param httpClientConfiguration the http client configuration * @return rest template instance */ @Bean @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) - public static RestTemplate restTemplate(final RestTemplateBuilder restTemplateBuilder) { - final RestTemplate restTemplate = restTemplateBuilder.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS) - .setReadTimeout(READ_TIMEOUT_MILLISECONDS).build(); + public static RestTemplate restTemplate(final RestTemplateBuilder restTemplateBuilder, + final HttpClientConfiguration httpClientConfiguration) { + + final ConnectionConfig connectionConfig = ConnectionConfig.copy(ConnectionConfig.DEFAULT) + .setConnectTimeout(Timeout.of(httpClientConfiguration.getConnectionTimeoutInSeconds())) + .build(); + + final PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() + .setDefaultConnectionConfig(connectionConfig) + .setMaxConnTotal(httpClientConfiguration.getMaximumConnectionsTotal()) + .setMaxConnPerRoute(httpClientConfiguration.getMaximumConnectionsPerRoute()) + .build(); + + final CloseableHttpClient httpClient = HttpClients.custom() + .setConnectionManager(connectionManager) + .evictExpiredConnections() + .evictIdleConnections( + TimeValue.of(httpClientConfiguration.getIdleConnectionEvictionThresholdInSeconds())) + .build(); + + final ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + + final RestTemplate restTemplate = restTemplateBuilder + .requestFactory(() -> requestFactory) + .setConnectTimeout(httpClientConfiguration.getConnectionTimeoutInSeconds()) + .build(); + setRestTemplateMessageConverters(restTemplate); return restTemplate; } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy index 51b00d143..013341f4b 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy @@ -212,7 +212,7 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification { 1 * mockLcmEventsCmHandleStateHandler.initiateStateAdvised(_) >> { args -> { - def cmHandleStatePerCmHandle = (args[0] as Map) + def cmHandleStatePerCmHandle = (args[0] as List) cmHandleStatePerCmHandle.each { assert (it.id == 'cmhandle' && it.dmiServiceName == 'my-server') } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy new file mode 100644 index 000000000..941c8b8a7 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy @@ -0,0 +1,48 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation. + * ================================================================================ + * 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 + * + * http://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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ +package org.onap.cps.ncmp.api.impl.config + +import java.time.Duration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import org.springframework.test.context.support.AnnotationConfigContextLoader +import spock.lang.Specification + +@SpringBootTest +@ContextConfiguration(classes = [HttpClientConfiguration]) +@EnableConfigurationProperties(HttpClientConfiguration.class) +@TestPropertySource(properties = ["httpclient5.connectionTimeoutInSeconds=1", "httpclient5.maximumConnectionsTotal=200"]) +class HttpClientConfigurationSpec extends Specification { + + @Autowired + private HttpClientConfiguration httpClientConfiguration + + def 'Test HttpClientConfiguration properties with custom and default values'() { + expect: 'custom property values' + httpClientConfiguration.getConnectionTimeoutInSeconds() == Duration.ofSeconds(1) + httpClientConfiguration.getMaximumConnectionsTotal() == 200 + and: 'default property values' + httpClientConfiguration.getMaximumConnectionsPerRoute() == 50 + httpClientConfiguration.getIdleConnectionEvictionThresholdInSeconds() == Duration.ofSeconds(5) + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy index e1aba79a5..a4df9b37c 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Nordix Foundation + * Copyright (C) 2021-2023 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,22 +19,27 @@ */ package org.onap.cps.ncmp.api.impl.config +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.web.client.RestTemplateBuilder import org.springframework.http.MediaType +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter import org.springframework.test.context.ContextConfiguration import org.springframework.web.client.RestTemplate import spock.lang.Specification @SpringBootTest -@ContextConfiguration(classes = [NcmpConfiguration.DmiProperties]) +@ContextConfiguration(classes = [NcmpConfiguration.DmiProperties, HttpClientConfiguration]) class NcmpConfigurationSpec extends Specification{ @Autowired NcmpConfiguration.DmiProperties dmiProperties - + + @Autowired + HttpClientConfiguration httpClientConfiguration + def mockRestTemplateBuilder = new RestTemplateBuilder() def 'NcmpConfiguration Construction.'() { @@ -48,11 +53,14 @@ class NcmpConfigurationSpec extends Specification{ dmiProperties.authPassword == 'some-password' } - def 'Rest Template creation.'() { + def 'Rest Template creation with CloseableHttpClient and MappingJackson2HttpMessageConverter.'() { when: 'a rest template is created' - def result = NcmpConfiguration.restTemplate(mockRestTemplateBuilder) + def result = NcmpConfiguration.restTemplate(mockRestTemplateBuilder, httpClientConfiguration) then: 'the rest template is returned' assert result instanceof RestTemplate + and: 'the rest template is created with httpclient5' + assert result.getRequestFactory() instanceof HttpComponentsClientHttpRequestFactory + assert ((HttpComponentsClientHttpRequestFactory) result.getRequestFactory()).getHttpClient() instanceof CloseableHttpClient; and: 'a jackson media converter has been added' def lastMessageConverter = result.getMessageConverters().get(result.getMessageConverters().size()-1) lastMessageConverter instanceof MappingJackson2HttpMessageConverter -- 2.16.6