From 3e65b5eeaee89758558667787e48a5ab9908a46f Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Mon, 12 Apr 2021 22:00:11 +0300 Subject: ActivityPub: pooling HTTP client --- .../java/com/juick/config/ActivityPubConfig.java | 30 ++---- .../java/com/juick/config/HttpClientConfig.java | 111 +++++++++++++++++++++ 2 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/juick/config/HttpClientConfig.java (limited to 'src/main/java') diff --git a/src/main/java/com/juick/config/ActivityPubConfig.java b/src/main/java/com/juick/config/ActivityPubConfig.java index 661f6276..430dc8d1 100644 --- a/src/main/java/com/juick/config/ActivityPubConfig.java +++ b/src/main/java/com/juick/config/ActivityPubConfig.java @@ -21,20 +21,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.juick.ActivityPubManager; import com.juick.www.api.activity.model.Activity; import com.juick.util.HeaderRequestInterceptor; -import org.apache.http.client.config.CookieSpecs; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.protocol.HttpClientContext; -import org.apache.http.protocol.HttpContext; +import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestTemplate; import javax.inject.Inject; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Collections; @@ -54,22 +49,17 @@ public class ActivityPubConfig { public ActivityPubManager activityPubManager() { return new ActivityPubManager(); } + @Inject + CloseableHttpClient httpClient; + @Bean + public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() { + HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(); + clientHttpRequestFactory.setHttpClient(httpClient); + return clientHttpRequestFactory; + } @Bean public RestTemplate apClient() { - RestTemplate restTemplate = new RestTemplate(); - restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory() { - @Override - protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) { - HttpClientContext context = HttpClientContext.create(); - context.setRequestConfig(getRequestConfig()); - return context; - } - RequestConfig getRequestConfig() { - RequestConfig.Builder builder = RequestConfig.custom() - .setCookieSpec(CookieSpecs.IGNORE_COOKIES); - return builder.build(); - } - }); + RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory()); restTemplate.getMessageConverters().add(0, mappingJacksonHttpMessageConverter()); restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8)); restTemplate.setErrorHandler(activityPubClientErrorHandler); diff --git a/src/main/java/com/juick/config/HttpClientConfig.java b/src/main/java/com/juick/config/HttpClientConfig.java new file mode 100644 index 00000000..0b23ca15 --- /dev/null +++ b/src/main/java/com/juick/config/HttpClientConfig.java @@ -0,0 +1,111 @@ +package com.juick.config; + +import java.util.concurrent.TimeUnit; + +import org.apache.http.HeaderElement; +import org.apache.http.HeaderElementIterator; +import org.apache.http.HttpResponse; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.conn.ConnectionKeepAliveStrategy; + +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.message.BasicHeaderElementIterator; +import org.apache.http.protocol.HTTP; +import org.apache.http.protocol.HttpContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +/** + * - Uses a connection pool to re-use connections and save overhead of creating connections. + * - Has a custom connection keep-alive strategy (to apply a default keep-alive if one isn't specified) + * - Starts an idle connection monitor to continuously clean up stale connections. + */ +@Configuration +public class HttpClientConfig { + + private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientConfig.class); + + // Determines the timeout in milliseconds until a connection is established. + private static final int CONNECT_TIMEOUT = 30000; + + // The timeout when requesting a connection from the connection manager. + private static final int REQUEST_TIMEOUT = 30000; + + // The timeout for waiting for data + private static final int SOCKET_TIMEOUT = 60000; + + private static final int MAX_TOTAL_CONNECTIONS = 50; + private static final int DEFAULT_KEEP_ALIVE_TIME_MILLIS = 20 * 1000; + private static final int CLOSE_IDLE_CONNECTION_WAIT_TIME_SECS = 30; + + @Bean + public PoolingHttpClientConnectionManager poolingConnectionManager() { + PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(); + poolingConnectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS); + return poolingConnectionManager; + } + + @Bean + public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() { + return new ConnectionKeepAliveStrategy() { + @Override + public long getKeepAliveDuration(HttpResponse response, HttpContext context) { + HeaderElementIterator it = new BasicHeaderElementIterator + (response.headerIterator(HTTP.CONN_KEEP_ALIVE)); + while (it.hasNext()) { + HeaderElement he = it.nextElement(); + String param = he.getName(); + String value = he.getValue(); + + if (value != null && param.equalsIgnoreCase("timeout")) { + return Long.parseLong(value) * 1000; + } + } + return DEFAULT_KEEP_ALIVE_TIME_MILLIS; + } + }; + } + + @Bean + public CloseableHttpClient httpClient() { + RequestConfig requestConfig = RequestConfig.custom() + .setCookieSpec(CookieSpecs.IGNORE_COOKIES) + .setConnectionRequestTimeout(REQUEST_TIMEOUT) + .setConnectTimeout(CONNECT_TIMEOUT) + .setSocketTimeout(SOCKET_TIMEOUT).build(); + + return HttpClients.custom() + .setDefaultRequestConfig(requestConfig) + .setConnectionManager(poolingConnectionManager()) + .setKeepAliveStrategy(connectionKeepAliveStrategy()) + .build(); + } + + @Bean + public Runnable idleConnectionMonitor(final PoolingHttpClientConnectionManager connectionManager) { + return new Runnable() { + @Override + @Scheduled(fixedDelay = 10000) + public void run() { + try { + if (connectionManager != null) { + LOGGER.trace("run IdleConnectionMonitor - Closing expired and idle connections..."); + connectionManager.closeExpiredConnections(); + connectionManager.closeIdleConnections(CLOSE_IDLE_CONNECTION_WAIT_TIME_SECS, TimeUnit.SECONDS); + } else { + LOGGER.trace("run IdleConnectionMonitor - Http Client Connection manager is not initialised"); + } + } catch (Exception e) { + LOGGER.error("run IdleConnectionMonitor - Exception occurred. msg={}, e={}", e.getMessage(), e); + } + } + }; + } +} \ No newline at end of file -- cgit v1.2.3