From 83e19359c762bd5652dfa8e2a66d7e5a0c3f2184 Mon Sep 17 00:00:00 2001
From: Thomas Lenz <thomas.lenz@egiz.gv.at>
Date: Thu, 5 Nov 2020 11:39:02 +0100
Subject: add scheduled eviction policy to clean-up expired or old http
 connections from pool

---
 .../eaaf/core/impl/http/HttpClientFactory.java     | 168 +++++++++++++--------
 1 file changed, 104 insertions(+), 64 deletions(-)

(limited to 'eaaf_core_utils/src/main')

diff --git a/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/http/HttpClientFactory.java b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/http/HttpClientFactory.java
index 647c0636..07522b56 100644
--- a/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/http/HttpClientFactory.java
+++ b/eaaf_core_utils/src/main/java/at/gv/egiz/eaaf/core/impl/http/HttpClientFactory.java
@@ -4,6 +4,8 @@ import java.security.KeyStore;
 import java.security.Provider;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
 
 import javax.annotation.Nonnull;
 import javax.annotation.PostConstruct;
@@ -23,6 +25,7 @@ import org.apache.http.client.methods.HttpUriRequest;
 import org.apache.http.config.Registry;
 import org.apache.http.config.RegistryBuilder;
 import org.apache.http.config.SocketConfig;
+import org.apache.http.conn.HttpClientConnectionManager;
 import org.apache.http.conn.socket.ConnectionSocketFactory;
 import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
 import org.apache.http.conn.socket.PlainConnectionSocketFactory;
@@ -33,10 +36,12 @@ import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.DefaultRedirectStrategy;
 import org.apache.http.impl.client.HttpClientBuilder;
 import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 import org.apache.http.protocol.HttpContext;
 import org.apache.http.ssl.SSLContexts;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
 
 import at.gv.egiz.eaaf.core.api.idp.IConfiguration;
 import at.gv.egiz.eaaf.core.exceptions.EaafConfigurationException;
@@ -65,10 +70,10 @@ public class HttpClientFactory implements IHttpClientFactory {
   public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_CONNECTION =
       "client.http.connection.timeout.connection";
   public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_REQUEST =
-      "client.http.connection.timeout.request";  
-  public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_COUNT = 
+      "client.http.connection.timeout.request";
+  public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_COUNT =
       "client.http.connection.retry.count";
-  public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_POST = 
+  public static final String PROP_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_POST =
       "client.http.connection.retry.post";
   public static final String PROP_CONFIG_CLIENT_HTTP_SSL_HOSTNAMEVERIFIER_TRUSTALL =
       "client.http.ssl.hostnameverifier.trustall";
@@ -97,9 +102,14 @@ public class HttpClientFactory implements IHttpClientFactory {
   public static final String DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXPERROUTE = "100";
   public static final String DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_COUNT = "3";
   public static final String DEFAUTL_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_POST = String.valueOf(false);
+
+  public static final int DEFAULT_CLEANUP_RUNNER_TIME = 30000;
+  public static final int DEFAULT_CLEANUP_IDLE_TIME = 60;
+  
   
   private String defaultConfigurationId = null;
-  private final Map<String, HttpClientBuilder> availableBuilders = new HashMap<>();
+  private final Map<String, Pair<HttpClientBuilder, HttpClientConnectionManager>> 
+      availableBuilders  = new HashMap<>();
 
   /*
    * (non-Javadoc)
@@ -114,7 +124,7 @@ public class HttpClientFactory implements IHttpClientFactory {
 
   @Override
   public CloseableHttpClient getHttpClient(final boolean followRedirects) {
-    return availableBuilders.get(defaultConfigurationId).setRedirectStrategy(
+    return availableBuilders.get(defaultConfigurationId).getFirst().setRedirectStrategy(
         buildRedirectStrategy(followRedirects)).build();
 
   }
@@ -124,30 +134,31 @@ public class HttpClientFactory implements IHttpClientFactory {
     log.trace("Build http client for: {}", config.getFriendlyName());
     HttpClientBuilder builder = null;
     if (availableBuilders.containsKey(config.getUuid())) {
-      builder = availableBuilders.get(config.getUuid());
+      builder = availableBuilders.get(config.getUuid()).getFirst();
 
     } else {
       log.debug("Initialize new http-client builder for: {}", config.getFriendlyName());
 
-      //validate configuration object
+      // validate configuration object
       config.validate();
 
       builder = HttpClients.custom();
-            
-      //inject request configuration
+
+      // inject request configuration
       builder.setDefaultRequestConfig(buildDefaultRequestConfig());
       injectInternalRetryHandler(builder, config);
-      
-      //inject basic authentication infos
+
+      // inject basic authentication infos
       injectBasicAuthenticationIfRequired(builder, config);
 
-      //inject authentication if required
+      // inject authentication if required
       final LayeredConnectionSocketFactory sslConnectionFactory = getSslContext(config);
 
       // set pool connection if required
-      injectDefaultConnectionPoolIfRequired(builder, sslConnectionFactory);
+      HttpClientConnectionManager connectionManager 
+          = injectConnectionManager(builder, sslConnectionFactory);
 
-      availableBuilders.put(config.getUuid(), builder);
+      availableBuilders.put(config.getUuid(), Pair.newInstance(builder, connectionManager));
 
     }
 
@@ -156,27 +167,45 @@ public class HttpClientFactory implements IHttpClientFactory {
 
   }
 
-  private void injectInternalRetryHandler(HttpClientBuilder builder, HttpClientConfiguration config) {    
+  /**
+   * Worker that closes expired connections or connections that in idle 
+   * for more than DEFAULT_CLEANUP_IDLE_TIME seconds.
+   * 
+   */
+  @Scheduled(fixedDelay = DEFAULT_CLEANUP_RUNNER_TIME)
+  private void httpConnectionPoolCleaner() {
+    log.trace("Starting http connection-pool eviction policy ... ");
+    for (final Entry<String, Pair<HttpClientBuilder, HttpClientConnectionManager>> el 
+        : availableBuilders.entrySet()) {
+      log.trace("Checking connections of http-client: {}", el.getKey());
+      el.getValue().getSecond().closeExpiredConnections();     
+      el.getValue().getSecond().closeIdleConnections(DEFAULT_CLEANUP_IDLE_TIME, TimeUnit.SECONDS);
+
+    }
+
+  }
+
+  private void injectInternalRetryHandler(HttpClientBuilder builder, HttpClientConfiguration config) {
     if (config.getHttpErrorRetryCount() > 0) {
-      log.info("Set HTTP error-retry to {} for http-client: {}", 
+      log.info("Set HTTP error-retry to {} for http-client: {}",
           config.getHttpErrorRetryCount(), config.getFriendlyName());
       builder.setRetryHandler(new EaafHttpRequestRetryHandler(
-          config.getHttpErrorRetryCount(), 
-          config.isHttpErrorRetryPost()));  
-      
+          config.getHttpErrorRetryCount(),
+          config.isHttpErrorRetryPost()));
+
       if (config.getServiceUnavailStrategy() != null) {
         log.debug("HttpClient configuration: {} set custom ServiceUnavailableRetryStrategy: {}",
             config.getFriendlyName(), config.getServiceUnavailStrategy().getClass().getName());
         builder.setServiceUnavailableRetryStrategy(config.getServiceUnavailStrategy());
-        
+
       }
-      
+
     } else {
       log.info("Disable HTTP error-retry for http-client: {}", config.getFriendlyName());
       builder.disableAutomaticRetries();
-      
+
     }
-    
+
   }
 
   @PostConstruct
@@ -190,8 +219,8 @@ public class HttpClientFactory implements IHttpClientFactory {
     // set default request configuration
     defaultHttpClientBuilder.setDefaultRequestConfig(buildDefaultRequestConfig());
     injectInternalRetryHandler(defaultHttpClientBuilder, defaultHttpClientConfig);
-    
-    //inject http basic authentication
+
+    // inject http basic authentication
     injectBasicAuthenticationIfRequired(defaultHttpClientBuilder, defaultHttpClientConfig);
 
     // inject authentication if required
@@ -199,11 +228,13 @@ public class HttpClientFactory implements IHttpClientFactory {
         getSslContext(defaultHttpClientConfig);
 
     // set pool connection if required
-    injectDefaultConnectionPoolIfRequired(defaultHttpClientBuilder, sslConnectionFactory);
+    HttpClientConnectionManager connectionManager 
+        = injectConnectionManager(defaultHttpClientBuilder, sslConnectionFactory);
 
-    //set default http client builder
+    // set default http client builder
     defaultConfigurationId = defaultHttpClientConfig.getUuid();
-    availableBuilders.put(defaultConfigurationId, defaultHttpClientBuilder);
+    availableBuilders.put(defaultConfigurationId, 
+        Pair.newInstance(defaultHttpClientBuilder, connectionManager));
 
   }
 
@@ -239,13 +270,12 @@ public class HttpClientFactory implements IHttpClientFactory {
         PROP_CONFIG_CLIENT_HTTP_SSL_HOSTNAMEVERIFIER_TRUSTALL, false));
 
     config.setHttpErrorRetryCount(Integer.parseInt(basicConfig.getBasicConfiguration(
-        PROP_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_COUNT, 
+        PROP_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_COUNT,
         DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_COUNT)));
     config.setHttpErrorRetryPost(Boolean.parseBoolean(basicConfig.getBasicConfiguration(
-        PROP_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_POST, 
+        PROP_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_POST,
         DEFAUTL_CONFIG_CLIENT_HTTP_CONNECTION_RETRY_POST)));
-    
-    
+
     // validate configuration object
     config.validate();
 
@@ -280,7 +310,8 @@ public class HttpClientFactory implements IHttpClientFactory {
     SSLContext sslContext = null;
     if (httpClientConfig.getAuthMode().equals(HttpClientConfiguration.ClientAuthMode.SSL)) {
       log.debug("Open keyStore with type: {}", httpClientConfig.getKeyStoreConfig().getKeyStoreType());
-      final Pair<KeyStore, Provider> keyStore = keyStoreFactory.buildNewKeyStore(httpClientConfig.getKeyStoreConfig());
+      final Pair<KeyStore, Provider> keyStore = keyStoreFactory.buildNewKeyStore(httpClientConfig
+          .getKeyStoreConfig());
 
       log.trace("Injecting SSL client-authentication into http client ... ");
       sslContext = HttpUtils.buildSslContextWithSslClientAuthentication(keyStore,
@@ -290,7 +321,7 @@ public class HttpClientFactory implements IHttpClientFactory {
     } else {
       log.trace("Initializing default SSL Context ... ");
       sslContext = SSLContexts.createDefault();
-     
+
     }
 
     // set hostname verifier
@@ -308,48 +339,37 @@ public class HttpClientFactory implements IHttpClientFactory {
 
   }
 
-  private void injectDefaultConnectionPoolIfRequired(
+  @Nonnull
+  private HttpClientConnectionManager injectConnectionManager(
       HttpClientBuilder builder, final LayeredConnectionSocketFactory sslConnectionFactory) {
     if (basicConfig.getBasicConfigurationBoolean(PROP_CONFIG_CLIENT_HTTP_CONNECTION_POOL_USE,
         true)) {
-      PoolingHttpClientConnectionManager pool;
-
-      // set socketFactoryRegistry if SSLConnectionFactory is Set
-      if (sslConnectionFactory != null) {
-        final Registry<ConnectionSocketFactory> socketFactoryRegistry =
-            RegistryBuilder.<ConnectionSocketFactory>create()
-                .register("http", PlainConnectionSocketFactory.getSocketFactory())
-                .register("https", sslConnectionFactory).build();
-        log.trace("Inject SSLSocketFactory into pooled connection");
-        pool = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
-
-      } else {
-        pool = new PoolingHttpClientConnectionManager();
-
-      }
-      
-      pool.setDefaultMaxPerRoute(Integer.parseInt(
+      PoolingHttpClientConnectionManager connectionPool 
+          = new PoolingHttpClientConnectionManager(getDefaultRegistry(sslConnectionFactory));
+      connectionPool.setDefaultMaxPerRoute(Integer.parseInt(
           basicConfig.getBasicConfiguration(PROP_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXPERROUTE,
               DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXPERROUTE)));
-      pool.setMaxTotal(Integer.parseInt(
+      connectionPool.setMaxTotal(Integer.parseInt(
           basicConfig.getBasicConfiguration(PROP_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXTOTAL,
               DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_POOL_MAXTOTAL)));
-
-      pool.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(Integer.parseInt(
+      connectionPool.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(Integer.parseInt(
           basicConfig.getBasicConfiguration(PROP_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_SOCKET,
               DEFAULT_CONFIG_CLIENT_HTTP_CONNECTION_TIMEOUT_SOCKET))
           * 1000).build());
+      builder.setConnectionManager(connectionPool);
+      log.debug("Initalize http-client pool with, maxTotal: {} maxPerRoute: {}", 
+          connectionPool.getMaxTotal(), connectionPool.getDefaultMaxPerRoute());
+      return connectionPool;
+      
+    } else {
+      log.debug("Building http-client without Connection-Pool ... ");
+      final BasicHttpClientConnectionManager basicPool = new BasicHttpClientConnectionManager(
+          getDefaultRegistry(sslConnectionFactory));      
+      builder.setConnectionManager(basicPool);      
+      return basicPool;
       
-      builder.setConnectionManager(pool);
-      log.debug("Initalize http-client pool with, maxTotal: {} maxPerRoute: {}", pool.getMaxTotal(),
-          pool.getDefaultMaxPerRoute());
-
-    } else if (sslConnectionFactory != null) {
-      log.trace("Inject SSLSocketFactory without connection pool");
-      builder.setSSLSocketFactory(sslConnectionFactory);
-
     }
-
+    
   }
 
   private RequestConfig buildDefaultRequestConfig() {
@@ -392,5 +412,25 @@ public class HttpClientFactory implements IHttpClientFactory {
     return redirectStrategy;
 
   }
+  
+  private static Registry<ConnectionSocketFactory> getDefaultRegistry(
+      final LayeredConnectionSocketFactory sslConnectionFactory) {
+    final RegistryBuilder<ConnectionSocketFactory> builder =
+        RegistryBuilder.<ConnectionSocketFactory>create()
+            .register("http", PlainConnectionSocketFactory.getSocketFactory());
+
+    if (sslConnectionFactory != null) {
+      log.trace("Inject own SSLSocketFactory into pooled connection");
+      builder.register("https", sslConnectionFactory);
+
+    } else {
+      log.trace("Inject default SSLSocketFactory into pooled connection");
+      builder.register("https", SSLConnectionSocketFactory.getSocketFactory());
+
+    }
+
+    return builder.build();
+
+  }
 
 }
-- 
cgit v1.2.3