/*
* Copyright 2013, The Sporting Exchange Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.betfair.cougar.client;
import com.betfair.cougar.client.api.GeoLocationSerializer;
import com.betfair.cougar.core.api.client.TransportMetrics;
import com.betfair.cougar.core.api.ev.ExecutionObserver;
import com.betfair.cougar.core.api.ev.OperationDefinition;
import com.betfair.cougar.core.api.exception.CougarFrameworkException;
import com.betfair.cougar.core.api.exception.ServerFaultCode;
import com.betfair.cougar.transport.api.protocol.http.HttpServiceBindingDescriptor;
import com.betfair.cougar.util.KeyStoreManagement;
import com.betfair.cougar.util.jmx.JMXControl;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.InputStreamResponseListener;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ExecutorThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.jmx.export.annotation.ManagedResource;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/**
* Implementation of client executable using async implementation of HTTP ReScript protocol.
*/
@ManagedResource
public class AsyncHttpExecutable extends AbstractHttpExecutable<Request> implements BeanNameAware {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientExecutable.class);
private HttpClient client;
private ExecutorService threadPool;
private final ExecutorService responseThreadPool;
private volatile boolean clientCreated;
private String beanName;
private JMXControl jmxControl;
private JettyTransportMetrics metrics;
private int maxConnectionsPerDestination;
private int maxRequestsQueuedPerDestination;
public AsyncHttpExecutable(HttpServiceBindingDescriptor bindingDescriptor, GeoLocationSerializer serializer,
ExecutorService threadPool, ExecutorService responseThreadPool) {
this(bindingDescriptor, serializer, DEFAULT_REQUEST_UUID_HEADER, threadPool, responseThreadPool);
}
public AsyncHttpExecutable(HttpServiceBindingDescriptor bindingDescriptor, GeoLocationSerializer serializer,
String requestUUIDHeader,
ExecutorService threadPool, ExecutorService responseThreadPool) {
super(bindingDescriptor, new JettyCougarRequestFactory(serializer, requestUUIDHeader));
((JettyCougarRequestFactory)super.requestFactory).setExecutable(this);
this.threadPool = threadPool;
this.responseThreadPool = responseThreadPool;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
@Override
public void init() throws Exception {
super.init();
if (client == null) {
client = new HttpClient(new SslContextFactory());
client.setExecutor(new ExecutorThreadPool(threadPool));
// configure timeout if set
if (connectTimeout != -1) {
client.setConnectTimeout(connectTimeout);
}
if (idleTimeout != -1) {
client.setIdleTimeout(idleTimeout);
}
client.setMaxConnectionsPerDestination(maxConnectionsPerDestination);
client.setMaxRequestsQueuedPerDestination(maxRequestsQueuedPerDestination);
//Configure SSL - if relevant
if (transportSSLEnabled) {
KeyStoreManagement keyStore = KeyStoreManagement.getKeyStoreManagement(httpsKeystoreType, httpsKeystore, httpsKeyPassword);
if (jmxControl != null && keyStore != null) {
jmxControl.registerMBean("CoUGAR:name=AsyncHttpClientKeyStore,beanName="+beanName, keyStore);
}
KeyStoreManagement trustStore = KeyStoreManagement.getKeyStoreManagement(httpsTruststoreType, httpsTruststore, httpsTrustPassword);
if (jmxControl != null) {
jmxControl.registerMBean("CoUGAR:name=AsyncHttpClientTrustStore,beanName="+beanName, trustStore);
}
if (trustStore == null) {
throw new IllegalStateException("This configuration ostensibly supports TLS, yet doesn't provide valid truststore configuration");
}
final SslContextFactory sslContextFactory = client.getSslContextFactory();
com.betfair.cougar.netutil.SslContextFactory factory = new com.betfair.cougar.netutil.SslContextFactory();
factory.setTrustManagerFactoryKeyStore(trustStore.getKeyStore());
if (keyStore != null) {
factory.setKeyManagerFactoryKeyStore(keyStore.getKeyStore());
factory.setKeyManagerFactoryKeyStorePassword(httpsKeyPassword);
}
SSLContext context = factory.newInstance();
if (hostnameVerificationDisabled) {
context.getDefaultSSLParameters().setEndpointIdentificationAlgorithm(null);
LOGGER.warn("CRITICAL SECURITY CHECKS ARE DISABLED: server SSL certificate hostname " +
"verification is turned off.");
}
else {
context.getDefaultSSLParameters().setEndpointIdentificationAlgorithm("https");
}
sslContextFactory.setSslContext(context);
}
client.start();
clientCreated = true;
}
metrics = new JettyTransportMetrics();
if (jmxControl != null) {
jmxControl.registerMBean("CoUGAR:name=AsyncHttpClientExecutable,beanName=" + beanName, this);
}
}
public void shutdown() throws Exception {
if (clientCreated) {
client.stop();
}
}
public void setJmxControl(JMXControl jmxControl) {
this.jmxControl = jmxControl;
}
public void setMaxConnectionsPerDestination(int maxConnectionsPerDestination) {
this.maxConnectionsPerDestination = maxConnectionsPerDestination;
}
public void setMaxRequestsQueuedPerDestination(int maxRequestsQueuedPerDestination) {
this.maxRequestsQueuedPerDestination = maxRequestsQueuedPerDestination;
}
@Override
protected void sendRequest(final Request request, final ExecutionObserver obs,
final OperationDefinition operationDefinition) {
final String url = String.valueOf(request.getURI());
final long startTime = System.currentTimeMillis();
InputStreamResponseListener listener = new InputStreamResponseListener() {
@Override
public void onHeaders(final Response response) {
super.onHeaders(response);
// can only get this once, so let's makes sure..
final InputStream inputStream = getInputStream();
// this needs to be done in a seperate thread pool - to avoid deadlocks it needs to be the same size as or larger than the client pool
responseThreadPool.submit(new Runnable() {
@Override
public void run() {
try {
processResponse(new CougarHttpResponse() {
@Override
public InputStream getEntity() throws IOException {
return inputStream;
}
@Override
public List<String> getContentEncoding() {
return new ArrayList<>(response.getHeaders().getValuesCollection(HttpHeader.CONTENT_ENCODING.asString()));
}
@Override
public int getResponseStatus() {
return response.getStatus();
}
@Override
public String getServerIdentity() {
return response.getHeaders().get(HttpHeader.SERVER);
}
@Override
public long getResponseSize() {
String s = response.getHeaders().get(HttpHeader.CONTENT_LENGTH);
return s != null ? Long.parseLong(s) : -1;
}
}, obs, operationDefinition);
}
catch (Exception e) {
LOGGER.warn("COUGAR: HTTP internal ERROR - URL [" + url + "] time [" + elapsed(startTime) + "mS]", e);
processException(obs, e, url);
}
}
});
}
};
request.onResponseFailure(new Response.FailureListener() {
@Override
public void onFailure(Response response, Throwable failure) {
ServerFaultCode faultCode = ServerFaultCode.RemoteCougarCommunicationFailure;
if (failure instanceof TimeoutException) {
failure = new CougarFrameworkException("Read timed out", failure);
faultCode = ServerFaultCode.Timeout;
}
LOGGER.warn("COUGAR: HTTP communication ERROR - URL [" + url + "] time [" + elapsed(startTime) + "mS]", failure);
processException(obs, failure, url, faultCode);
}
}).onRequestFailure(new Request.FailureListener() {
@Override
public void onFailure(Request request, Throwable failure) {
LOGGER.warn("COUGAR: HTTP connection FAILED - URL [" + url + "] time [" + elapsed(startTime) + " mS]", failure);
processException(obs, failure, url);
}
}).send(listener);
}
private long elapsed(long startTime) {
return System.currentTimeMillis() - startTime;
}
protected boolean gzipHandledByTransport() {
return true;
}
@Override
public TransportMetrics getTransportMetrics() {
return metrics;
}
public void setClient(HttpClient client) {
this.client = client;
}
//@VisibleForTesting
HttpClient getClient() {
return client;
}
final class JettyTransportMetrics implements TransportMetrics {
private int port;
private String host;
private boolean https;
private Method HttpDestination_getActiveConnections;
private Method HttpDestination_getIdleConnections;
JettyTransportMetrics() {
try {
URI uri = new URI(getRemoteAddress());
https = "https".equalsIgnoreCase(uri.getScheme());
port = uri.getPort() < 0 ? (https ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT) : uri.getPort();
host = uri.getHost();
} catch (URISyntaxException e) {
throw new RuntimeException("Unable to determine address and port for service address '" + getRemoteAddress() + "'");//NOSONAR
}
try {
HttpDestination_getActiveConnections = HttpDestination.class.getDeclaredMethod("getActiveConnections");
HttpDestination_getActiveConnections.setAccessible(true);
HttpDestination_getIdleConnections = HttpDestination.class.getDeclaredMethod("getIdleConnections");
HttpDestination_getIdleConnections.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
private HttpDestination getDestination() {
return (HttpDestination) client.getDestination(https ? "https" : "http", host, port);
}
@Override
public int getOpenConnections() {
try {
if (host != null) {
return ((BlockingQueue)HttpDestination_getActiveConnections.invoke(getDestination())).size();
}
} catch (InvocationTargetException | IllegalAccessException ite) {
LOGGER.warn("Could not determine open connections for '" + getRemoteAddress() + "'");
}
return 0;
}
@Override
public int getMaximumConnections() {
return maxConnectionsPerDestination;
}
@Override
public int getFreeConnections() {
try {
if (host != null) {
return ((BlockingQueue)HttpDestination_getIdleConnections.invoke(getDestination())).size();
}
} catch (InvocationTargetException | IllegalAccessException ite) {
LOGGER.warn("Could not determine open connections for '" + getRemoteAddress() + "'");
}
return 0;
}
@Override
public int getCurrentLoad() {
return (getOpenConnections() - getFreeConnections()) * 100 / getMaximumConnections();
}
}
}