package com.netflix.discovery.shared.transport.jersey2;
import static com.netflix.discovery.util.DiscoveryBuildInfo.buildVersion;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.params.CoreProtocolPNames;
import org.glassfish.jersey.apache.connector.ApacheClientProperties;
import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.discovery.converters.wrappers.CodecWrappers;
import com.netflix.discovery.converters.wrappers.DecoderWrapper;
import com.netflix.discovery.converters.wrappers.EncoderWrapper;
import com.netflix.discovery.provider.DiscoveryJerseyProvider;
import com.netflix.servo.monitor.BasicCounter;
import com.netflix.servo.monitor.BasicTimer;
import com.netflix.servo.monitor.Counter;
import com.netflix.servo.monitor.MonitorConfig;
import com.netflix.servo.monitor.Monitors;
import com.netflix.servo.monitor.Stopwatch;
/**
* @author Tomasz Bak
*/
public class EurekaJersey2ClientImpl implements EurekaJersey2Client {
private static final Logger s_logger = LoggerFactory.getLogger(EurekaJersey2ClientImpl.class);
private static final int HTTP_CONNECTION_CLEANER_INTERVAL_MS = 30 * 1000;
private static final String PROTOCOL = "https";
private static final String PROTOCOL_SCHEME = "SSL";
private static final int HTTPS_PORT = 443;
private static final String KEYSTORE_TYPE = "JKS";
private final Client apacheHttpClient;
private final ConnectionCleanerTask connectionCleanerTask;
ClientConfig jerseyClientConfig;
private final ScheduledExecutorService eurekaConnCleaner =
Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "Eureka-Jersey2Client-Conn-Cleaner" + threadNumber.incrementAndGet());
thread.setDaemon(true);
return thread;
}
});
public EurekaJersey2ClientImpl(
int connectionTimeout,
int readTimeout,
final int connectionIdleTimeout,
ClientConfig clientConfig) {
try {
jerseyClientConfig = clientConfig;
jerseyClientConfig.register(DiscoveryJerseyProvider.class);
jerseyClientConfig.connectorProvider(new ApacheConnectorProvider());
jerseyClientConfig.property(ClientProperties.CONNECT_TIMEOUT, connectionTimeout);
jerseyClientConfig.property(ClientProperties.READ_TIMEOUT, readTimeout);
apacheHttpClient = ClientBuilder.newClient(jerseyClientConfig);
connectionCleanerTask = new ConnectionCleanerTask(connectionIdleTimeout);
eurekaConnCleaner.scheduleWithFixedDelay(
connectionCleanerTask, HTTP_CONNECTION_CLEANER_INTERVAL_MS,
HTTP_CONNECTION_CLEANER_INTERVAL_MS,
TimeUnit.MILLISECONDS);
} catch (Throwable e) {
throw new RuntimeException("Cannot create Jersey2 client", e);
}
}
@Override
public Client getClient() {
return apacheHttpClient;
}
/**
* Clean up resources.
*/
@Override
public void destroyResources() {
if (eurekaConnCleaner != null) {
// Execute the connection cleaner one final time during shutdown
eurekaConnCleaner.execute(connectionCleanerTask);
eurekaConnCleaner.shutdown();
}
if (apacheHttpClient != null) {
apacheHttpClient.close();
}
}
public static class EurekaJersey2ClientBuilder {
private boolean systemSSL;
private String clientName;
private int maxConnectionsPerHost;
private int maxTotalConnections;
private String trustStoreFileName;
private String trustStorePassword;
private String userAgent;
private String proxyUserName;
private String proxyPassword;
private String proxyHost;
private String proxyPort;
private int connectionTimeout;
private int readTimeout;
private int connectionIdleTimeout;
private EncoderWrapper encoderWrapper;
private DecoderWrapper decoderWrapper;
public EurekaJersey2ClientBuilder withClientName(String clientName) {
this.clientName = clientName;
return this;
}
public EurekaJersey2ClientBuilder withUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public EurekaJersey2ClientBuilder withConnectionTimeout(int connectionTimeout) {
this.connectionTimeout = connectionTimeout;
return this;
}
public EurekaJersey2ClientBuilder withReadTimeout(int readTimeout) {
this.readTimeout = readTimeout;
return this;
}
public EurekaJersey2ClientBuilder withConnectionIdleTimeout(int connectionIdleTimeout) {
this.connectionIdleTimeout = connectionIdleTimeout;
return this;
}
public EurekaJersey2ClientBuilder withMaxConnectionsPerHost(int maxConnectionsPerHost) {
this.maxConnectionsPerHost = maxConnectionsPerHost;
return this;
}
public EurekaJersey2ClientBuilder withMaxTotalConnections(int maxTotalConnections) {
this.maxTotalConnections = maxTotalConnections;
return this;
}
public EurekaJersey2ClientBuilder withProxy(String proxyHost, String proxyPort, String user, String password) {
this.proxyHost = proxyHost;
this.proxyPort = proxyPort;
this.proxyUserName = user;
this.proxyPassword = password;
return this;
}
public EurekaJersey2ClientBuilder withSystemSSLConfiguration() {
this.systemSSL = true;
return this;
}
public EurekaJersey2ClientBuilder withTrustStoreFile(String trustStoreFileName, String trustStorePassword) {
this.trustStoreFileName = trustStoreFileName;
this.trustStorePassword = trustStorePassword;
return this;
}
public EurekaJersey2ClientBuilder withEncoder(String encoderName) {
return this.withEncoderWrapper(CodecWrappers.getEncoder(encoderName));
}
public EurekaJersey2ClientBuilder withEncoderWrapper(EncoderWrapper encoderWrapper) {
this.encoderWrapper = encoderWrapper;
return this;
}
public EurekaJersey2ClientBuilder withDecoder(String decoderName, String clientDataAccept) {
return this.withDecoderWrapper(CodecWrappers.resolveDecoder(decoderName, clientDataAccept));
}
public EurekaJersey2ClientBuilder withDecoderWrapper(DecoderWrapper decoderWrapper) {
this.decoderWrapper = decoderWrapper;
return this;
}
public EurekaJersey2Client build() {
MyDefaultApacheHttpClient4Config config = new MyDefaultApacheHttpClient4Config();
try {
return new EurekaJersey2ClientImpl(
connectionTimeout,
readTimeout,
connectionIdleTimeout,
config);
} catch (Throwable e) {
throw new RuntimeException("Cannot create Jersey client ", e);
}
}
class MyDefaultApacheHttpClient4Config extends ClientConfig {
MyDefaultApacheHttpClient4Config() {
PoolingHttpClientConnectionManager cm;
if (systemSSL) {
cm = createSystemSslCM();
} else if (trustStoreFileName != null) {
cm = createCustomSslCM();
} else {
cm = new PoolingHttpClientConnectionManager();
}
if (proxyHost != null) {
addProxyConfiguration();
}
DiscoveryJerseyProvider discoveryJerseyProvider = new DiscoveryJerseyProvider(encoderWrapper, decoderWrapper);
// getSingletons().add(discoveryJerseyProvider);
register(discoveryJerseyProvider);
// Common properties to all clients
cm.setDefaultMaxPerRoute(maxConnectionsPerHost);
cm.setMaxTotal(maxTotalConnections);
property(ApacheClientProperties.CONNECTION_MANAGER, cm);
String fullUserAgentName = (userAgent == null ? clientName : userAgent) + "/v" + buildVersion();
property(CoreProtocolPNames.USER_AGENT, fullUserAgentName);
// To pin a client to specific server in case redirect happens, we handle redirects directly
// (see DiscoveryClient.makeRemoteCall methods).
property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE);
property(ClientPNames.HANDLE_REDIRECTS, Boolean.FALSE);
}
private void addProxyConfiguration() {
if (proxyUserName != null && proxyPassword != null) {
property(ClientProperties.PROXY_USERNAME, proxyUserName);
property(ClientProperties.PROXY_PASSWORD, proxyPassword);
} else {
// Due to bug in apache client, user name/password must always be set.
// Otherwise proxy configuration is ignored.
property(ClientProperties.PROXY_USERNAME, "guest");
property(ClientProperties.PROXY_PASSWORD, "guest");
}
property(ClientProperties.PROXY_URI, "http://" + proxyHost + ":" + proxyPort);
}
private PoolingHttpClientConnectionManager createSystemSslCM() {
ConnectionSocketFactory socketFactory = SSLConnectionSocketFactory.getSystemSocketFactory();
Registry registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register(PROTOCOL, socketFactory)
.build();
return new PoolingHttpClientConnectionManager(registry);
}
private PoolingHttpClientConnectionManager createCustomSslCM() {
FileInputStream fin = null;
try {
SSLContext sslContext = SSLContext.getInstance(PROTOCOL_SCHEME);
KeyStore sslKeyStore = KeyStore.getInstance(KEYSTORE_TYPE);
fin = new FileInputStream(trustStoreFileName);
sslKeyStore.load(fin, trustStorePassword.toCharArray());
TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
factory.init(sslKeyStore);
TrustManager[] trustManagers = factory.getTrustManagers();
sslContext.init(null, trustManagers, null);
ConnectionSocketFactory socketFactory =
new SSLConnectionSocketFactory(sslContext, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
Registry registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register(PROTOCOL, socketFactory)
.build();
return new PoolingHttpClientConnectionManager(registry);
} catch (Exception ex) {
throw new IllegalStateException("SSL configuration issue", ex);
} finally {
if (fin != null) {
try {
fin.close();
} catch (IOException ignore) {
}
}
}
}
}
}
private class ConnectionCleanerTask implements Runnable {
private final int connectionIdleTimeout;
private final BasicTimer executionTimeStats;
private final Counter cleanupFailed;
private ConnectionCleanerTask(int connectionIdleTimeout) {
this.connectionIdleTimeout = connectionIdleTimeout;
MonitorConfig.Builder monitorConfigBuilder = MonitorConfig.builder("Eureka-Connection-Cleaner-Time");
executionTimeStats = new BasicTimer(monitorConfigBuilder.build());
cleanupFailed = new BasicCounter(MonitorConfig.builder("Eureka-Connection-Cleaner-Failure").build());
try {
Monitors.registerObject(this);
} catch (Exception e) {
s_logger.error("Unable to register with servo.", e);
}
}
@Override
public void run() {
Stopwatch start = executionTimeStats.start();
try {
HttpClientConnectionManager cm = (HttpClientConnectionManager) apacheHttpClient
.getConfiguration()
.getProperty(ApacheClientProperties.CONNECTION_MANAGER);
cm.closeIdleConnections(connectionIdleTimeout, TimeUnit.SECONDS);
} catch (Throwable e) {
s_logger.error("Cannot clean connections", e);
cleanupFailed.increment();
} finally {
if (null != start) {
start.stop();
}
}
}
}
}