All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.nhenneaux.resilienthttpclient.monitoredclientpool.HttpClientPool Maven / Gradle / Ivy

package com.github.nhenneaux.resilienthttpclient.monitoredclientpool;

import com.github.nhenneaux.resilienthttpclient.singlehostclient.DnsLookupWrapper;
import com.github.nhenneaux.resilienthttpclient.singlehostclient.ServerConfiguration;

import java.net.InetAddress;
import java.net.http.HttpClient;
import java.security.Security;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static java.util.function.Predicate.not;

/**
 * A HTTP clients pool which keeps internally a round robin list of HTTP clients.
* Each HTTP client represents a connection to the acquirer using a distinct IP address, taken from endpoint resolving.
* The number of distinct HTTP clients in this connection pool is equal to the number of different IP addresses for the given acquirer hostname. */ public class HttpClientPool implements AutoCloseable { private static final Logger LOGGER = Logger.getLogger(HttpClientPool.class.getSimpleName()); private final AtomicReference httpClientsCache; private final ServerConfiguration serverConfiguration; private final ScheduledFuture scheduledFutureDnsRefresh; protected HttpClientPool( final DnsLookupWrapper dnsLookupWrapper, final ScheduledExecutorService scheduledExecutorService, final ServerConfiguration serverConfiguration, final Function singleHttpClientProvider ) { this.serverConfiguration = serverConfiguration; this.httpClientsCache = new AtomicReference<>(); checkDnsCacheSecurityProperties(); // We schedule a refresh of DNS lookup to catch this change // Existing HTTP clients for which InetAddress is still present in the list will be kept // HttpClients for which the ip has disappeared will be closed // For the new IPs new Http clients will be created final long dnsLookupRefreshPeriodInSeconds = serverConfiguration.getDnsLookupRefreshPeriodInSeconds(); this.scheduledFutureDnsRefresh = scheduledExecutorService.scheduleAtFixedRate( () -> refreshTheListWrappedInTryCatch( dnsLookupWrapper, serverConfiguration, httpClientsCache, singleHttpClientProvider, scheduledExecutorService ), dnsLookupRefreshPeriodInSeconds, dnsLookupRefreshPeriodInSeconds, TimeUnit.SECONDS ); // We invoke the same method here as in the scheduler. We don't want constructor to crush in case of a temporary issue. // However for misconfiguration problems it will re-throw an exception and crush the constructor. refreshTheListWrappedInTryCatch(dnsLookupWrapper, serverConfiguration, httpClientsCache, singleHttpClientProvider, scheduledExecutorService); } public static HttpClientPoolBuilder builder(final ServerConfiguration serverConfiguration) { return new HttpClientPoolBuilder(serverConfiguration); } public static HttpClientPool newHttpClientPool(final ServerConfiguration serverConfiguration) { return new HttpClientPoolBuilder(serverConfiguration).build(); } static boolean validateProperty(String propertyName, int minimumPropertyValueExpected) { final String propertyValue = Security.getProperty(propertyName); if (propertyValue != null && !propertyValue.isEmpty() && ("-1".equals(propertyValue) || Integer.parseInt(propertyValue) > minimumPropertyValueExpected) ) { LOGGER.log(Level.SEVERE, () -> "The JVM Security property '" + propertyName + "' is set to '" + propertyValue + "' while a value greater than '" + minimumPropertyValueExpected + "' is expected."); return false; } return true; } private static void refreshTheListWrappedInTryCatch( final DnsLookupWrapper dnsLookupWrapper, final ServerConfiguration serverConfiguration, final AtomicReference httpClientsCache, final Function singleHttpClientProvider, final ScheduledExecutorService scheduledExecutorService ) { try { refreshTheList(dnsLookupWrapper, serverConfiguration, httpClientsCache, singleHttpClientProvider, scheduledExecutorService); } catch (IllegalArgumentException e) { // IllegalArgumentException means a misconfiguration and has to be re-thrown immediately throw e; } catch (RuntimeException e) { LOGGER.log(Level.SEVERE, "Error while refreshing list of IP clients: " + e.getMessage(), e); } } private static void refreshTheList( final DnsLookupWrapper dnsLookupWrapper, final ServerConfiguration serverConfiguration, final AtomicReference httpClientsCache, final Function singleHttpClientProvider, final ScheduledExecutorService scheduledExecutorService ) { final List oldListOfClients = Optional.ofNullable(httpClientsCache.get()) .orElse(RoundRobinPool.EMPTY) .getList(); final String hostname = serverConfiguration.getHostname(); final Set updatedLookup = dnsLookupWrapper.getInetAddressesByDnsLookUp(hostname); if (updatedLookup.isEmpty()) { if (oldListOfClients.isEmpty()) { LOGGER.log(Level.SEVERE, "The DNS lookup has returned an empty list of IPs. There is no client in the pool."); } else { LOGGER.log(Level.WARNING, "The DNS lookup has returned an empty list of IPs. Reusing the old list."); } return; } httpClientsCache.set(new RoundRobinPool( updatedLookup .stream() .map(inetAddress -> useOldClientOrCreateNew( singleHttpClientProvider, inetAddress, oldListOfClients, serverConfiguration, scheduledExecutorService )) .collect(Collectors.toUnmodifiableList()) )); // Close those clients whose inet address is not present anymore oldListOfClients.stream() .filter(not(client -> updatedLookup.contains(client.getInetAddress()))) .forEach(oldClient -> { LOGGER.log(Level.INFO, () -> "The IP " + oldClient.getInetAddress().getHostAddress() + " for hostname " + hostname + " is not present in the DNS resolution list any more, closing the HttpClient"); oldClient.close(); }); } private static SingleIpHttpClient useOldClientOrCreateNew( final Function singleHttpClientProvider, final InetAddress inetAddress, final List oldListOfClients, final ServerConfiguration serverConfiguration, final ScheduledExecutorService scheduledExecutorService ) { // Try to find the client with the same inetAddress in the old list and reuse it or build a new one return oldListOfClients.stream() .filter(oldClient -> oldClient.getInetAddress().getHostAddress().equals(inetAddress.getHostAddress())) .findAny() .orElseGet(() -> { LOGGER.log(Level.INFO, () -> "New IP found: " + inetAddress.getHostAddress() + " for hostname " + serverConfiguration.getHostname() + ", creating a new HttpClient"); return new SingleIpHttpClient( singleHttpClientProvider.apply(inetAddress), inetAddress, serverConfiguration, scheduledExecutorService ); }); } private void checkDnsCacheSecurityProperties() { // Default "networkaddress.cache.ttl" is 30 seconds, "-1" means cache forever validateProperty("networkaddress.cache.ttl", 60); // Default "networkaddress.cache.negative.ttl" is 10 seconds, "-1" means cache forever validateProperty("networkaddress.cache.negative.ttl", 11); } /** * Take the next HTTP client from the pool.
* Please note that it uses a round robin internally. So once it reaches the end of the list it starts returning items from the beginning and so on. */ public Optional getNextHttpClient() { return client().next(); } /** * Return a resilient client with the following features. *

* DNS failover if an IP resolved by DNS is not reachable it automatically fallbacks to another IP *

* Monitored each IP connection to the server is monitored in HTTP *

* Load balanced the traffic is load balanced on DNS records */ public HttpClient resilientClient() { return new ResilientClient(this::client); } private RoundRobinPool client() { return Optional.ofNullable(httpClientsCache.get()).orElse(RoundRobinPool.EMPTY); } /** * Returns status {@link HealthCheckResult.HealthStatus#OK} if all httpClientsCache are healthy.
* Returns status {@link HealthCheckResult.HealthStatus#ERROR} if all httpClientsCache are unhealthy.
* Returns status {@link HealthCheckResult.HealthStatus#WARNING} if only some httpClientsCache are healthy.
*/ public HealthCheckResult check() { final List clients = client().getList(); LOGGER.log(Level.FINE, () -> "Check HTTP clients pool for health connection(s): " + clients); final boolean allConnectionsAvailable = clients.stream().allMatch(SingleIpHttpClient::isHealthy); final boolean allConnectionsUnavailable = clients.stream().noneMatch(SingleIpHttpClient::isHealthy); final HealthCheckResult.HealthStatus status; if (allConnectionsUnavailable) { status = HealthCheckResult.HealthStatus.ERROR; } else if (allConnectionsAvailable) { status = HealthCheckResult.HealthStatus.OK; } else { status = HealthCheckResult.HealthStatus.WARNING; } LOGGER.log(Level.FINE, () -> "HTTP clients pool health is " + status); return new HealthCheckResult(status, clients .stream() .map(SingleIpHttpClient::toString) .collect(Collectors.toList())); } @Override public String toString() { return "HttpClientPool{" + "httpClientsCache=" + httpClientsCache + ", serverConfiguration=" + serverConfiguration + '}'; } @Override public void close() { scheduledFutureDnsRefresh.cancel(true); client().getList().forEach(SingleIpHttpClient::close); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy