se.fortnox.reactivewizard.client.RxClientProvider Maven / Gradle / Ivy
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