package com.facebook.presto.jdbc.internal.airlift.http.client.netty;
import com.facebook.presto.jdbc.internal.guava.annotations.Beta;
import com.facebook.presto.jdbc.internal.guava.annotations.VisibleForTesting;
import com.facebook.presto.jdbc.internal.guava.base.Preconditions;
import com.facebook.presto.jdbc.internal.guava.base.Throwables;
import com.facebook.presto.jdbc.internal.guava.collect.ImmutableList;
import com.facebook.presto.jdbc.internal.airlift.concurrent.ThreadPoolExecutorMBean;
import com.facebook.presto.jdbc.internal.airlift.http.client.AsyncHttpClient;
import com.facebook.presto.jdbc.internal.airlift.http.client.BodyGenerator;
import com.facebook.presto.jdbc.internal.airlift.http.client.HttpClientConfig;
import com.facebook.presto.jdbc.internal.airlift.http.client.HttpRequestFilter;
import com.facebook.presto.jdbc.internal.airlift.http.client.Request;
import com.facebook.presto.jdbc.internal.airlift.http.client.RequestStats;
import com.facebook.presto.jdbc.internal.airlift.http.client.ResponseHandler;
import com.facebook.presto.jdbc.internal.airlift.http.client.netty.NettyConnectionPool.ConnectionCallback;
import com.facebook.presto.jdbc.internal.airlift.http.client.netty.NettyResponseFuture.NettyAsyncHttpState;
import com.facebook.presto.jdbc.internal.airlift.http.client.netty.socks.Socks4ClientBootstrap;
import com.facebook.presto.jdbc.internal.netty.bootstrap.ClientBootstrap;
import com.facebook.presto.jdbc.internal.netty.buffer.ChannelBufferOutputStream;
import com.facebook.presto.jdbc.internal.netty.buffer.DynamicChannelBuffer;
import com.facebook.presto.jdbc.internal.netty.channel.Channel;
import com.facebook.presto.jdbc.internal.netty.channel.ChannelFactory;
import com.facebook.presto.jdbc.internal.netty.channel.ChannelFuture;
import com.facebook.presto.jdbc.internal.netty.channel.ChannelFutureListener;
import com.facebook.presto.jdbc.internal.netty.channel.socket.nio.NioClientSocketChannelFactory;
import com.facebook.presto.jdbc.internal.netty.handler.codec.http.DefaultHttpRequest;
import com.facebook.presto.jdbc.internal.netty.handler.codec.http.HttpHeaders.Names;
import com.facebook.presto.jdbc.internal.netty.handler.codec.http.HttpMethod;
import com.facebook.presto.jdbc.internal.netty.handler.codec.http.HttpRequest;
import com.facebook.presto.jdbc.internal.netty.handler.codec.http.HttpVersion;
import com.facebook.presto.jdbc.internal.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor;
import com.facebook.presto.jdbc.internal.netty.util.HashedWheelTimer;
import org.weakref.jmx.Flatten;
import org.weakref.jmx.Managed;
import org.weakref.jmx.Nested;
import javax.annotation.PreDestroy;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import static com.facebook.presto.jdbc.internal.airlift.concurrent.Threads.daemonThreadsNamed;
@Beta
public class NettyAsyncHttpClient
implements AsyncHttpClient
{
private final RequestStats stats = new RequestStats();
private final List<HttpRequestFilter> requestFilters;
private final OrderedMemoryAwareThreadPoolExecutor executor;
private final ThreadPoolExecutorMBean executorMBean;
private final NettyConnectionPool nettyConnectionPool;
private final HashedWheelTimer timer;
public NettyAsyncHttpClient(String name, HttpClientConfig config, NettyIoPool ioPool)
{
this(name, ioPool, config, new NettyAsyncHttpClientConfig(), Collections.<HttpRequestFilter>emptySet());
}
public NettyAsyncHttpClient(String name,
NettyIoPool ioPool,
HttpClientConfig config,
NettyAsyncHttpClientConfig asyncConfig,
Set<? extends HttpRequestFilter> requestFilters)
{
Preconditions.checkNotNull(name, "name is null");
Preconditions.checkNotNull(ioPool, "ioPool is null");
Preconditions.checkNotNull(config, "config is null");
Preconditions.checkNotNull(asyncConfig, "asyncConfig is null");
Preconditions.checkNotNull(requestFilters, "requestFilters is null");
this.requestFilters = ImmutableList.copyOf(requestFilters);
String namePrefix = "http-client-" + name;
// shared timer for channel factory and read timeout channel handler
this.timer = new HashedWheelTimer(daemonThreadsNamed(namePrefix + "-timer-%s"));
ChannelFactory channelFactory = new NioClientSocketChannelFactory(ioPool.getBossPool(), ioPool.getWorkerPool());
ThreadFactory workerThreadFactory = daemonThreadsNamed(namePrefix + "-worker-%s");
this.executor = new OrderedMemoryAwareThreadPoolExecutor(asyncConfig.getWorkerThreads(), 0, 0, 30, TimeUnit.SECONDS, workerThreadFactory);
this.executorMBean = new ThreadPoolExecutorMBean(executor);
ClientBootstrap bootstrap;
if (config.getSocksProxy() == null) {
bootstrap = new ClientBootstrap(channelFactory);
}
else {
bootstrap = new Socks4ClientBootstrap(channelFactory, config.getSocksProxy());
}
bootstrap.setOption("connectTimeoutMillis", config.getConnectTimeout().toMillis());
bootstrap.setOption("soLinger", 0);
nettyConnectionPool = new NettyConnectionPool(bootstrap,
config.getMaxConnections(),
executor,
asyncConfig.isEnableConnectionPooling());
HttpClientPipelineFactory pipelineFactory = new HttpClientPipelineFactory(nettyConnectionPool, timer, executor, config.getReadTimeout(), asyncConfig.getMaxContentLength());
bootstrap.setPipelineFactory(pipelineFactory);
}
public List<HttpRequestFilter> getRequestFilters()
{
return requestFilters;
}
@SuppressWarnings("deprecation")
@PreDestroy
@Override
public void close()
{
try {
executor.shutdownNow();
}
catch (Exception e) {
// ignored
}
try {
nettyConnectionPool.close();
}
catch (Exception e) {
// ignored
}
try {
timer.stop();
}
catch (Exception e) {
// ignored
}
}
@Override
public <T, E extends Exception> T execute(Request request, ResponseHandler<T, E> responseHandler)
throws E
{
try {
return executeAsync(request, responseHandler).get();
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw Throwables.propagate(e);
}
catch (ExecutionException e) {
Throwables.propagateIfPossible(e.getCause());
if (e.getCause() instanceof Exception) {
// the HTTP client and ResponseHandler interface enforces this
throw (E) e.getCause();
}
// e.getCause() is some direct subclass of throwable
throw Throwables.propagate(e.getCause());
}
}
@Managed
@Flatten
@Override
public RequestStats getStats()
{
return stats;
}
@Managed
@Nested
public ThreadPoolExecutorMBean getExecutor()
{
return executorMBean;
}
@Override
public <T, E extends Exception> AsyncHttpResponseFuture<T> executeAsync(Request request, ResponseHandler<T, E> responseHandler)
{
// process the request through the filters
for (HttpRequestFilter requestFilter : requestFilters) {
request = requestFilter.filterRequest(request);
}
Preconditions.checkArgument("http".equalsIgnoreCase(request.getUri().getScheme()) || "https".equalsIgnoreCase(request.getUri().getScheme()),
"%s only supports http and https requests", getClass().getSimpleName());
// create a future for the caller
NettyResponseFuture<T, E> nettyResponseFuture = new NettyResponseFuture<>(request, responseHandler, stats);
// schedule the request with a connection
nettyConnectionPool.execute(request.getUri(), new HttpConnectionCallback<>(request, nettyResponseFuture));
// return caller's future
return nettyResponseFuture;
}
@VisibleForTesting
public static HttpRequest buildNettyHttpRequest(Request request)
throws Exception
{
//
// http request path
URI uri = request.getUri();
StringBuilder pathBuilder = new StringBuilder(100);
// path part
if (uri.getRawPath() == null || uri.getRawPath().isEmpty()) {
pathBuilder.append('/');
}
else {
pathBuilder.append(uri.getRawPath());
}
// query
if (uri.getRawQuery() != null) {
pathBuilder.append('?').append(uri.getRawQuery());
}
// http clients should not send the #fragment
//
// set http request line
HttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod(request.getMethod()), pathBuilder.toString());
//
// set host header
if (uri.getPort() == -1) {
nettyRequest.setHeader(Names.HOST, uri.getHost());
}
else {
nettyRequest.setHeader(Names.HOST, uri.getHost() + ":" + uri.getPort());
}
//
// set user defined headers
for (Entry<String, Collection<String>> header : request.getHeaders().asMap().entrySet()) {
nettyRequest.setHeader(header.getKey(), header.getValue());
}
//
// set body
BodyGenerator bodyGenerator = request.getBodyGenerator();
if (bodyGenerator != null) {
DynamicChannelBuffer content = new DynamicChannelBuffer(64 * 1024);
ChannelBufferOutputStream out = new ChannelBufferOutputStream(content);
bodyGenerator.write(out);
nettyRequest.setHeader(Names.CONTENT_LENGTH, content.readableBytes());
nettyRequest.setContent(content);
}
return nettyRequest;
}
private static class HttpConnectionCallback<T, E extends Exception>
implements ConnectionCallback
{
private final Request request;
private final NettyResponseFuture<T, E> nettyResponseFuture;
public HttpConnectionCallback(Request request, NettyResponseFuture<T, E> nettyResponseFuture)
{
this.request = request;
this.nettyResponseFuture = nettyResponseFuture;
}
@Override
public void run(Channel channel)
throws Exception
{
nettyResponseFuture.setState(NettyAsyncHttpState.SENDING_REQUEST);
// add the response handler to the channel object, so we can notify caller when request is complete
channel.getPipeline().getContext(NettyHttpResponseChannelHandler.class).setAttachment(nettyResponseFuture);
HttpRequest nettyRequest = buildNettyHttpRequest(request);
channel.write(nettyRequest).addListener(new ChannelFutureListener()
{
@Override
public void operationComplete(ChannelFuture future)
throws Exception
{
if (future.isSuccess()) {
nettyResponseFuture.setState(NettyAsyncHttpState.WAITING_FOR_RESPONSE);
}
else if (future.isCancelled()) {
nettyResponseFuture.failed(new CanceledRequestException());
}
else {
Throwable cause = future.getCause();
if (cause == null) {
cause = new UnknownRequestException();
}
nettyResponseFuture.failed(cause);
}
}
});
}
@Override
public void onError(Throwable throwable)
{
nettyResponseFuture.failed(throwable);
}
}
}