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

se.fortnox.reactivewizard.client.RxClientProvider Maven / Gradle / Ivy

There is a newer version: 24.6.0
Show newest version
package se.fortnox.reactivewizard.client;

import com.codahale.metrics.Gauge;
import io.netty.buffer.ByteBuf;
import io.reactivex.netty.client.ConnectionProviderFactory;
import io.reactivex.netty.client.Host;
import io.reactivex.netty.client.pool.MaxConnectionsBasedStrategy;
import io.reactivex.netty.client.pool.PoolConfig;
import io.reactivex.netty.client.pool.SingleHostPoolingProviderFactory;
import io.reactivex.netty.protocol.http.client.HttpClient;
import io.reactivex.netty.protocol.http.internal.UnsubscribeAwareHttpClientToConnectionBridge;
import se.fortnox.reactivewizard.metrics.HealthRecorder;
import se.fortnox.reactivewizard.metrics.Metrics;

import javax.inject.Inject;
import javax.inject.Singleton;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import static rx.Observable.just;

/**
 * Provides httpclients for use in proxies. This needs to be a singleton as the
 * maxConnections limit is set per client. It also needs to handle both ssl and
 * non-ssl clients as this may differ between calls.
 */
@Singleton
public class RxClientProvider {
    private final ConcurrentHashMap> clients    = new ConcurrentHashMap<>();
    private final HttpClientConfig                                                   config;
    private final HealthRecorder                                                     healthRecorder;

    @Inject
    public RxClientProvider(HttpClientConfig config, HealthRecorder healthRecorder) {
        this.config = config;
        this.healthRecorder = healthRecorder;
    }

    public HttpClient clientFor(InetSocketAddress serverInfo) {
        return clients.computeIfAbsent(serverInfo, this::buildClient);
    }

    private HttpClient configureSsl(HttpClient client, String host, int port, boolean isValidateCertificates) {
        if (!isValidateCertificates) {
            return client.unsafeSecure();
        }
        try {
            return client.secure((ignored) -> createSslEngineForEachConnection(host, port));
        } catch (Throwable e) {
            throw new RuntimeException("Unable to create secure https client.", e);
        }
    }

    private HttpClient buildClient(InetSocketAddress socketAddress) {
        PoolConfig poolConfig = new PoolConfig<>();
        poolConfig.limitDeterminationStrategy(new MetricPublishingMaxConnectionsBasedStrategy(config.getMaxConnections(), healthRecorder));
        ConnectionProviderFactory connectionProviderFactory = createConnectionProviderFactory(poolConfig);

        ConnectionProviderFactory pool = new UnsubscribeAwareConnectionProviderFactory(connectionProviderFactory, poolConfig);

        HttpClient client = HttpClient.newClient(pool, just(new Host(socketAddress)))
            .readTimeOut(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS)
            .followRedirects(false)
            .pipelineConfigurator(UnsubscribeAwareHttpClientToConnectionBridge::configurePipeline);

        if (config.isHttps()) {
            return configureSsl(client, config.getHost(), config.getPort(), config.isValidateCertificates());
        }
        return client;
    }


    /**
     * Allows for customization of the SSLEngine.
     *
     * @return An configured SSLEngine
     */
    SSLEngine configureSslEngine(String host, int port) {
        try {
            return SSLContext.getDefault().createSSLEngine(host, port);
        } catch (Throwable e) {
            throw new RuntimeException("Unable to configure secure client", e);
        }
    }

    protected ConnectionProviderFactory createConnectionProviderFactory(PoolConfig poolConfig) {
        return SingleHostPoolingProviderFactory.create(poolConfig);
    }

    /**
     * Factory method that provides a new sslEngine for each connection.
     *
     * It prevents the issue when the SSL engine thinks that handshake has been made, but in fact, the connection is new.
     *
     * @return a new SSLEngine instance.
     */
    private SSLEngine createSslEngineForEachConnection(String host,int port) {
        SSLEngine sslEngine = configureSslEngine(host, port);
        sslEngine.setUseClientMode(true);
        return sslEngine;
    }

    /**
     * Logs number of available connections in pool and reports unhealthy when pool is exhausted.
     */
    private static class MetricPublishingMaxConnectionsBasedStrategy extends MaxConnectionsBasedStrategy {
        private final HealthRecorder healthRecorder;

        public MetricPublishingMaxConnectionsBasedStrategy(int maxConnections, HealthRecorder healthRecorder) {
            super(maxConnections);
            this.healthRecorder = healthRecorder;
            Metrics.registry().register(
                "http_client_permits_id:" + this.hashCode(),
                (Gauge)this::getAvailablePermits
            );
        }

        @Override
        public boolean acquireCreationPermit(long acquireStartTime, TimeUnit timeUnit) {
            return healthRecorder.logStatus(this, super.acquireCreationPermit(acquireStartTime, timeUnit));
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy