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

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

There is a newer version: 1.0.4
Show newest version
package com.github.nhenneaux.resilienthttpclient.monitoredclientpool;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import java.io.IOException;
import java.net.Authenticator;
import java.net.ConnectException;
import java.net.CookieHandler;
import java.net.InetAddress;
import java.net.ProxySelector;
import java.net.http.HttpClient;
import java.net.http.HttpConnectTimeoutException;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.WebSocket;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Logger;

class ResilientClient extends HttpClient {

    private static final Logger LOGGER = Logger.getLogger(ResilientClient.class.getName());
    private static final Set> CONNECT_EXCEPTION_CLASS = Set.of(HttpConnectTimeoutException.class, ConnectException.class);
    private final Supplier roundRobinPoolSupplier;

    ResilientClient(Supplier roundRobinPoolSupplier) {
        this.roundRobinPoolSupplier = roundRobinPoolSupplier;
    }

    @Override
    public Optional cookieHandler() {
        return httpClient().cookieHandler();
    }

    private static SingleIpHttpClient singleIpHttpClient(RoundRobinPool roundRobinPool) {
        return roundRobinPool.next().orElseThrow(() -> new IllegalStateException("There is no healthy connection to send the request"));
    }

    static  CompletableFuture> handleConnectTimeout(Function>> send, RoundRobinPool roundRobinPool) {
        final SingleIpHttpClient firstClient = singleIpHttpClient(roundRobinPool);
        return handleConnectTimeout(send, roundRobinPool, firstClient, new ArrayList<>());

    }

    @Override
    public Optional connectTimeout() {
        return httpClient().connectTimeout();
    }

    @Override
    public Redirect followRedirects() {
        return httpClient().followRedirects();
    }

    @Override
    public Optional proxy() {
        return httpClient().proxy();
    }

    @Override
    public SSLContext sslContext() {
        return httpClient().sslContext();
    }

    @Override
    public SSLParameters sslParameters() {
        return httpClient().sslParameters();
    }

    @Override
    public Optional authenticator() {
        return httpClient().authenticator();
    }

    @Override
    public Version version() {
        return httpClient().version();
    }

    @Override
    public Optional executor() {
        return httpClient().executor();
    }

    @SuppressWarnings("squid:S3864")// Usage of peek method is correct here
    private static  CompletableFuture> handleConnectTimeout(
            Function>> send,
            RoundRobinPool roundRobinPool,
            SingleIpHttpClient firstClient,
            List triedAddress
    ) {
        final long healthyNodes = roundRobinPool.getList().stream().filter(SingleIpHttpClient::isHealthy).count();
        if (triedAddress.size() >= healthyNodes) {
            final CompletableFuture> httpResponseCompletableFuture = new CompletableFuture<>();
            httpResponseCompletableFuture.completeExceptionally(new HttpConnectTimeoutException("Cannot connect to the server, the following address were tried without success " + triedAddress + "."));
            return httpResponseCompletableFuture;
        }
        return Optional.of(firstClient)
                .filter(ignored -> triedAddress.isEmpty())
                .or(roundRobinPool::next)
                .stream()
                .peek(singleIpHttpClient -> triedAddress.add(singleIpHttpClient.getInetAddress()))
                .map(SingleIpHttpClient::getHttpClient)
                .map(send)
                .map(httpResponseFuture -> httpResponseFuture.exceptionally(throwable -> {
                    if (Optional.ofNullable(throwable.getCause())
                            .map(Object::getClass)
                            .filter(CONNECT_EXCEPTION_CLASS::contains)
                            .isPresent()
                    ) {
                        return handleConnectTimeout(send, roundRobinPool, firstClient, triedAddress).join();
                    }
                    if (throwable instanceof Error) {
                        throw (Error) throwable;
                    }
                    if (throwable instanceof RuntimeException) {
                        throw (RuntimeException) throwable;
                    }
                    throw new IllegalStateException(throwable);
                }))
                .findAny()
                .orElseThrow(() -> new IllegalStateException("Cannot connect to the server, the following address were tried without success " + triedAddress + "."));
    }


    private HttpClient httpClient() {
        return singleIpHttpClient(roundRobinPoolSupplier.get()).getHttpClient();
    }

    @Override
    public  HttpResponse send(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) throws IOException, InterruptedException {
        final RoundRobinPool roundRobinPool = roundRobinPoolSupplier.get();
        final SingleIpHttpClient firstClient = roundRobinPool.next().orElseThrow(() -> new IllegalStateException("There is no healthy connection to send the request in the pool " + roundRobinPool));
        final long healthyNodes = roundRobinPool.getList().stream().filter(SingleIpHttpClient::isHealthy).count();
        final List tried = new ArrayList<>();


        SingleIpHttpClient client = firstClient;
        while (tried.size() < healthyNodes) {
            try {
                return client.getHttpClient().send(request, responseBodyHandler);
            } catch (HttpConnectTimeoutException | ConnectException e) {
                var finalClient = client;
                LOGGER.warning(() -> "Got a connect timeout when trying to connect to " + finalClient.getInetAddress() + ", already tried " + tried);
                tried.add(finalClient.getInetAddress());
                final Optional nextClient = roundRobinPool.next();
                if (nextClient.isEmpty()) {
                    final HttpConnectTimeoutException httpConnectTimeoutException = new HttpConnectTimeoutException("Cannot connect to the HTTP server, tried to connect to the following IP " + tried + " to send the HTTP request " + request);
                    httpConnectTimeoutException.initCause(e);
                    throw httpConnectTimeoutException;
                }
                client = nextClient.get();
            }
        }
        throw new HttpConnectTimeoutException("Cannot connect to the HTTP server, tried to connect to the following IP " + tried + " to send the HTTP request " + request);
    }

    @Override
    public  CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) {
        return handleConnectTimeout(httpclient -> httpclient.sendAsync(request, responseBodyHandler), roundRobinPoolSupplier.get());

    }

    @Override
    public  CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler, HttpResponse.PushPromiseHandler pushPromiseHandler) {
        return handleConnectTimeout(httpclient -> httpclient.sendAsync(request, responseBodyHandler, pushPromiseHandler), roundRobinPoolSupplier.get());
    }

    @Override
    public WebSocket.Builder newWebSocketBuilder() {
        return httpClient().newWebSocketBuilder();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy