
com.proofpoint.http.client.jetty.JettyHttpClient Maven / Gradle / Ivy
package com.proofpoint.http.client.jetty;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.io.Closeables;
import com.google.common.io.CountingInputStream;
import com.google.common.net.HostAndPort;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.AbstractFuture;
import com.proofpoint.http.client.BodySource;
import com.proofpoint.http.client.DynamicBodySource;
import com.proofpoint.http.client.DynamicBodySource.Writer;
import com.proofpoint.http.client.HttpClientConfig;
import com.proofpoint.http.client.HttpRequestFilter;
import com.proofpoint.http.client.InputStreamBodySource;
import com.proofpoint.http.client.Request;
import com.proofpoint.http.client.RequestStats;
import com.proofpoint.http.client.ResponseHandler;
import com.proofpoint.http.client.ResponseTooLargeException;
import com.proofpoint.http.client.StaticBodyGenerator;
import com.proofpoint.log.Logger;
import com.proofpoint.stats.Distribution;
import com.proofpoint.tracetoken.TraceTokenScope;
import com.proofpoint.units.Duration;
import org.eclipse.jetty.client.DuplexConnectionPool;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.client.PoolingHttpDestination;
import org.eclipse.jetty.client.Socks4Proxy;
import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Response.Listener;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.client.http.HttpConnectionOverHTTP;
import org.eclipse.jetty.client.http.HttpDestinationOverHTTP;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.client.util.InputStreamContentProvider;
import org.eclipse.jetty.client.util.InputStreamResponseListener;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.ArrayQueue;
import org.eclipse.jetty.util.HttpCookieStore;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.Sweeper;
import org.weakref.jmx.Flatten;
import org.weakref.jmx.Managed;
import org.weakref.jmx.Nested;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.propagate;
import static com.proofpoint.tracetoken.TraceTokenManager.getCurrentRequestToken;
import static com.proofpoint.tracetoken.TraceTokenManager.registerRequestToken;
import static java.lang.Math.min;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
public class JettyHttpClient
implements com.proofpoint.http.client.HttpClient
{
private static final String[] DISABLED_CIPHERS = {
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
"TLS_ECDHE_RSA_WITH_RC4_128_SHA",
"SSL_RSA_WITH_RC4_128_SHA",
"TLS_ECDH_ECDSA_WITH_RC4_128_SHA",
"TLS_ECDH_RSA_WITH_RC4_128_SHA",
"SSL_RSA_WITH_RC4_128_MD5",
"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256",
"TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
"TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256",
"TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
"TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384",
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256",
"SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
"SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
};
private static final AtomicLong nameCounter = new AtomicLong();
private static final String PLATFORM_STATS_KEY = "platform_stats";
private static final long SWEEP_PERIOD_MILLIS = 5000;
private final HttpClient httpClient;
private final long maxContentLength;
private final Long requestTimeoutMillis;
private final long idleTimeoutMillis;
private final RequestStats stats = new RequestStats();
private final CachedDistribution queuedRequestsPerDestination;
private final CachedDistribution activeConnectionsPerDestination;
private final CachedDistribution idleConnectionsPerDestination;
private final CachedDistribution currentQueuedTime;
private final CachedDistribution currentRequestTime;
private final CachedDistribution currentRequestSendTime;
private final CachedDistribution currentResponseWaitTime;
private final CachedDistribution currentResponseProcessTime;
private final List requestFilters;
private final Exception creationLocation = new Exception();
private final String name;
public JettyHttpClient()
{
this(new HttpClientConfig(), ImmutableList.of());
}
public JettyHttpClient(HttpClientConfig config)
{
this(config, ImmutableList.of());
}
public JettyHttpClient(HttpClientConfig config, Iterable extends HttpRequestFilter> requestFilters)
{
this(config, Optional.absent(), requestFilters);
}
public JettyHttpClient(HttpClientConfig config, JettyIoPool jettyIoPool, Iterable extends HttpRequestFilter> requestFilters)
{
this(config, Optional.of(jettyIoPool), requestFilters);
}
private JettyHttpClient(HttpClientConfig config, Optional jettyIoPool, Iterable extends HttpRequestFilter> requestFilters)
{
checkNotNull(config, "config is null");
checkNotNull(jettyIoPool, "jettyIoPool is null");
checkNotNull(requestFilters, "requestFilters is null");
maxContentLength = config.getMaxContentLength().toBytes();
Duration requestTimeout = config.getRequestTimeout();
if (requestTimeout == null) {
requestTimeoutMillis = null;
}
else {
requestTimeoutMillis = requestTimeout.toMillis();
}
idleTimeoutMillis = config.getIdleTimeout().toMillis();
creationLocation.fillInStackTrace();
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setEndpointIdentificationAlgorithm("HTTPS");
sslContextFactory.addExcludeProtocols("SSLv3", "SSLv2Hello");
sslContextFactory.addExcludeCipherSuites(DISABLED_CIPHERS);
String[] excludeCipherSuites = sslContextFactory.getExcludeCipherSuites();
for (int i = 0; i < excludeCipherSuites.length; i++) {
if ("^.*_RSA_.*_(MD5|SHA|SHA1)$".equals(excludeCipherSuites[i])) {
excludeCipherSuites[i] = "^.*_RSA_.*_MD5$";
}
}
sslContextFactory.setExcludeCipherSuites(excludeCipherSuites);
if (config.getKeyStorePath() != null) {
sslContextFactory.setKeyStorePath(config.getKeyStorePath());
sslContextFactory.setKeyStorePassword(config.getKeyStorePassword());
}
if (config.getMaxRequestsQueuedPerDestination() == 0) {
httpClient = new HttpClient(
new HttpClientTransportOverHTTP(2)
{
@Override
public HttpDestination newHttpDestination(Origin origin)
{
return new LimitQueuedToAvailableConnectionsHttpDestination(config.getMaxConnectionsPerServer(), getHttpClient(), origin);
}
},
sslContextFactory);
httpClient.setMaxRequestsQueuedPerDestination(config.getMaxConnectionsPerServer());
}
else {
httpClient = new HttpClient(new HttpClientTransportOverHTTP(2), sslContextFactory);
httpClient.setMaxRequestsQueuedPerDestination(config.getMaxRequestsQueuedPerDestination());
}
httpClient.setMaxConnectionsPerDestination(config.getMaxConnectionsPerServer());
// disable cookies
httpClient.setCookieStore(new HttpCookieStore.Empty());
// timeouts
httpClient.setIdleTimeout(idleTimeoutMillis);
httpClient.setConnectTimeout(config.getConnectTimeout().toMillis());
httpClient.setAddressResolutionTimeout(config.getConnectTimeout().toMillis());
if (config.getConnectTimeout() != null) {
long connectTimeout = config.getConnectTimeout().toMillis();
httpClient.setConnectTimeout(connectTimeout);
httpClient.setAddressResolutionTimeout(connectTimeout);
}
HostAndPort socksProxy = config.getSocksProxy();
if (socksProxy != null) {
httpClient.getProxyConfiguration().getProxies().add(new Socks4Proxy(socksProxy.getHostText(), socksProxy.getPortOrDefault(1080)));
}
JettyIoPool pool = jettyIoPool.orNull();
if (pool == null) {
pool = new JettyIoPool("anonymous" + nameCounter.incrementAndGet(), new JettyIoPoolConfig());
}
name = pool.getName();
this.httpClient.setExecutor(pool.getExecutor());
this.httpClient.setByteBufferPool(pool.getByteBufferPool());
this.httpClient.setScheduler(pool.getScheduler());
// Jetty client connections can sometimes get stuck while closing which reduces
// the available connections. The Jetty Sweeper periodically scans the active
// connection pool looking for connections in the closed state, and if a connection
// is observed in the closed state multiple times, it logs, and destroys the connection.
httpClient.addBean(new Sweeper(pool.getScheduler(), SWEEP_PERIOD_MILLIS), true);
try {
this.httpClient.start();
// remove the GZIP encoding from the client
// TODO: there should be a better way to to do this
this.httpClient.getContentDecoderFactories().clear();
}
catch (Exception e) {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
throw propagate(e);
}
this.requestFilters = ImmutableList.copyOf(requestFilters);
this.activeConnectionsPerDestination = new ConnectionPoolDistribution(httpClient,
(distribution, connectionPool) -> distribution.add(connectionPool.getActiveConnections().size()));
this.idleConnectionsPerDestination = new ConnectionPoolDistribution(httpClient,
(distribution, connectionPool) -> distribution.add(connectionPool.getIdleConnections().size()));
this.queuedRequestsPerDestination = new DestinationDistribution(httpClient,
(distribution, destination) -> distribution.add(destination.getHttpExchanges().size()));
this.currentQueuedTime = new RequestDistribution(httpClient, (distribution, listener, now) -> {
long started = listener.getRequestStarted();
if (started == 0) {
started = now;
}
distribution.add(NANOSECONDS.toMillis(started - listener.getCreated()));
});
this.currentRequestTime = new RequestDistribution(httpClient, (distribution, listener, now) -> {
long started = listener.getRequestStarted();
if (started == 0) {
return;
}
long finished = listener.getResponseFinished();
if (finished == 0) {
finished = now;
}
distribution.add(NANOSECONDS.toMillis(finished - started));
});
this.currentRequestSendTime = new RequestDistribution(httpClient, (distribution, listener, now) -> {
long started = listener.getRequestStarted();
if (started == 0) {
return;
}
long requestSent = listener.getRequestFinished();
if (requestSent == 0) {
requestSent = now;
}
distribution.add(NANOSECONDS.toMillis(requestSent - started));
});
this.currentResponseWaitTime = new RequestDistribution(httpClient, (distribution, listener, now) -> {
long requestSent = listener.getRequestFinished();
if (requestSent == 0) {
return;
}
long responseStarted = listener.getResponseStarted();
if (responseStarted == 0) {
responseStarted = now;
}
distribution.add(NANOSECONDS.toMillis(responseStarted - requestSent));
});
this.currentResponseProcessTime = new RequestDistribution(httpClient, (distribution, listener, now) -> {
long responseStarted = listener.getResponseStarted();
if (responseStarted == 0) {
return;
}
long finished = listener.getResponseFinished();
if (finished == 0) {
finished = now;
}
distribution.add(NANOSECONDS.toMillis(finished - responseStarted));
});
}
@Override
public T execute(Request request, ResponseHandler responseHandler)
throws E
{
long requestStart = System.nanoTime();
AtomicLong bytesWritten = new AtomicLong(0);
// apply filters
request = applyRequestFilters(request);
// create jetty request and response listener
HttpRequest jettyRequest = buildJettyRequest(request, bytesWritten);
InputStreamResponseListener listener = new InputStreamResponseListener(maxContentLength)
{
@Override
public void onContent(Response response, ByteBuffer content)
{
// ignore empty blocks
if (content.remaining() == 0) {
return;
}
super.onContent(response, content);
}
};
// fire the request
jettyRequest.send(listener);
// wait for response to begin
Response response;
try {
response = listener.get(httpClient.getIdleTimeout(), MILLISECONDS);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
return responseHandler.handleException(request, e);
}
catch (TimeoutException e) {
return responseHandler.handleException(request, e);
}
catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof Exception) {
return responseHandler.handleException(request, (Exception) cause);
}
else {
return responseHandler.handleException(request, new RuntimeException(cause));
}
}
// process response
long responseStart = System.nanoTime();
JettyResponse jettyResponse = null;
T value;
try {
InputStream inputStream = listener.getInputStream();
try {
jettyResponse = new JettyResponse(response, inputStream);
value = responseHandler.handle(request, jettyResponse);
}
finally {
Closeables.closeQuietly(inputStream);
}
}
finally {
recordRequestComplete(stats, request, requestStart, bytesWritten.get(), jettyResponse, responseStart);
}
return value;
}
@Override
public HttpResponseFuture executeAsync(Request request, ResponseHandler responseHandler)
{
checkNotNull(request, "request is null");
checkNotNull(responseHandler, "responseHandler is null");
AtomicLong bytesWritten = new AtomicLong(0);
request = applyRequestFilters(request);
HttpRequest jettyRequest = buildJettyRequest(request, bytesWritten);
JettyResponseFuture future = new JettyResponseFuture<>(request, jettyRequest, responseHandler, bytesWritten, stats);
BufferingResponseListener listener = new BufferingResponseListener(future, Ints.saturatedCast(maxContentLength));
try {
jettyRequest.send(listener);
}
catch (RuntimeException e) {
if (!(e instanceof RejectedExecutionException)) {
e = new RejectedExecutionException(e);
}
// normally this is a rejected execution exception because the client has been closed
future.failed(e);
}
return future;
}
private Request applyRequestFilters(Request request)
{
for (HttpRequestFilter requestFilter : requestFilters) {
request = requestFilter.filterRequest(request);
}
return request;
}
private HttpRequest buildJettyRequest(Request finalRequest, AtomicLong bytesWritten)
{
HttpRequest jettyRequest = (HttpRequest) httpClient.newRequest(finalRequest.getUri());
JettyRequestListener listener = new JettyRequestListener(finalRequest.getUri());
jettyRequest.onRequestBegin(request -> listener.onRequestBegin());
jettyRequest.onRequestSuccess(request -> listener.onRequestEnd());
jettyRequest.onResponseBegin(response -> listener.onResponseBegin());
jettyRequest.onComplete(result -> listener.onFinish());
jettyRequest.attribute(PLATFORM_STATS_KEY, listener);
// jetty client always adds the user agent header
// todo should there be a default?
jettyRequest.getHeaders().remove(HttpHeader.USER_AGENT);
jettyRequest.method(finalRequest.getMethod());
for (Entry entry : finalRequest.getHeaders().entries()) {
jettyRequest.header(entry.getKey(), entry.getValue());
}
BodySource bodySource = finalRequest.getBodySource();
if (bodySource != null) {
if (bodySource instanceof StaticBodyGenerator) {
StaticBodyGenerator staticBodyGenerator = (StaticBodyGenerator) bodySource;
jettyRequest.content(new BytesContentProvider(staticBodyGenerator.getBody()));
bytesWritten.addAndGet(staticBodyGenerator.getBody().length);
}
else if (bodySource instanceof InputStreamBodySource) {
InputStreamBodySource inputStreamBodySource = (InputStreamBodySource) bodySource;
jettyRequest.content(new InputStreamContentProvider(new BodySourceInputStream(inputStreamBodySource, bytesWritten), inputStreamBodySource.getBufferSize(), false));
}
else if (bodySource instanceof DynamicBodySource) {
jettyRequest.content(new DynamicBodySourceContentProvider((DynamicBodySource) bodySource, bytesWritten));
}
else {
throw new IllegalArgumentException("Request has unsupported BodySource type");
}
}
jettyRequest.followRedirects(finalRequest.isFollowRedirects());
// timeouts
if (requestTimeoutMillis != null) {
jettyRequest.timeout(requestTimeoutMillis, MILLISECONDS);
}
jettyRequest.idleTimeout(idleTimeoutMillis, MILLISECONDS);
return jettyRequest;
}
public List getRequestFilters()
{
return requestFilters;
}
@Override
@Managed
@Flatten
public RequestStats getStats()
{
return stats;
}
@Managed
@Nested
public CachedDistribution getActiveConnectionsPerDestination()
{
return activeConnectionsPerDestination;
}
@Managed
@Nested
public CachedDistribution getIdleConnectionsPerDestination()
{
return idleConnectionsPerDestination;
}
@Managed
@Nested
public CachedDistribution getQueuedRequestsPerDestination()
{
return queuedRequestsPerDestination;
}
@Managed
@Nested
public CachedDistribution getCurrentQueuedTime()
{
return currentQueuedTime;
}
@Managed
@Nested
public CachedDistribution getCurrentRequestTime()
{
return currentRequestTime;
}
@Managed
@Nested
public CachedDistribution getCurrentRequestSendTime()
{
return currentRequestSendTime;
}
@Managed
@Nested
public CachedDistribution getCurrentResponseWaitTime()
{
return currentResponseWaitTime;
}
@Managed
@Nested
public CachedDistribution getCurrentResponseProcessTime()
{
return currentResponseProcessTime;
}
@Managed
public String dump()
{
return httpClient.dump();
}
@Managed
public void dumpStdErr()
{
httpClient.dumpStdErr();
}
@Managed
public String dumpAllDestinations()
{
return String.format("%s\t%s\t%s\t%s\t%s%n", "URI", "queued", "request", "wait", "response") +
httpClient.getDestinations().stream()
.map(JettyHttpClient::dumpDestination)
.collect(Collectors.joining("\n"));
}
// todo this should be @Managed but operations with parameters are broken in jmx utils https://github.com/martint/jmxutils/issues/27
@SuppressWarnings("UnusedDeclaration")
public String dumpDestination(URI uri)
{
Destination destination = httpClient.getDestination(uri.getScheme(), uri.getHost(), uri.getPort());
if (destination == null) {
return null;
}
return dumpDestination(destination);
}
private static String dumpDestination(Destination destination)
{
long now = System.nanoTime();
return getRequestListenersForDestination(destination).stream()
.map(request -> dumpRequest(now, request))
.sorted()
.collect(Collectors.joining("\n"));
}
private static List getRequestListenersForDestination(Destination destination)
{
return getRequestForDestination(destination).stream()
.map(request -> (JettyRequestListener) request.getAttributes().get(PLATFORM_STATS_KEY))
.filter(listener -> listener != null)
.collect(Collectors.toList());
}
private static List getRequestForDestination(Destination destination)
{
PoolingHttpDestination> poolingHttpDestination = (PoolingHttpDestination>) destination;
Queue httpExchanges = poolingHttpDestination.getHttpExchanges();
List requests = httpExchanges.stream()
.map(HttpExchange::getRequest)
.collect(Collectors.toList());
poolingHttpDestination.getConnectionPool().getActiveConnections().stream()
.filter(HttpConnectionOverHTTP.class::isInstance)
.map(connection -> ((HttpConnectionOverHTTP) connection).getHttpChannel().getHttpExchange())
.filter(exchange -> exchange != null)
.forEach(exchange -> requests.add(exchange.getRequest()));
return requests.stream().filter(request -> request != null).collect(Collectors.toList());
}
private static String dumpRequest(long now, JettyRequestListener listener)
{
long created = listener.getCreated();
long requestStarted = listener.getRequestStarted();
if (requestStarted == 0) {
requestStarted = now;
}
long requestFinished = listener.getRequestFinished();
if (requestFinished == 0) {
requestFinished = now;
}
long responseStarted = listener.getResponseStarted();
if (responseStarted == 0) {
responseStarted = now;
}
long finished = listener.getResponseFinished();
if (finished == 0) {
finished = now;
}
return String.format("%s\t%.1f\t%.1f\t%.1f\t%.1f",
listener.getUri(),
nanosToMillis(requestStarted - created),
nanosToMillis(requestFinished - requestStarted),
nanosToMillis(responseStarted - requestFinished),
nanosToMillis(finished - responseStarted));
}
private static double nanosToMillis(long nanos)
{
return new Duration(nanos, NANOSECONDS).getValue(MILLISECONDS);
}
@Override
public void close()
{
try {
httpClient.stop();
}
catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
catch (Exception ignored) {
}
}
@Override
public String toString()
{
return Objects.toStringHelper(this)
.addValue(name)
.toString();
}
@SuppressWarnings("UnusedDeclaration")
public StackTraceElement[] getCreationLocation()
{
return creationLocation.getStackTrace();
}
private static class JettyResponse
implements com.proofpoint.http.client.Response
{
private final Response response;
private final CountingInputStream inputStream;
JettyResponse(Response response, InputStream inputStream)
{
this.response = response;
this.inputStream = new CountingInputStream(inputStream);
}
@Override
public int getStatusCode()
{
return response.getStatus();
}
@Override
public String getStatusMessage()
{
return response.getReason();
}
@Override
public String getHeader(String name)
{
return response.getHeaders().get(name);
}
@Override
public ListMultimap getHeaders()
{
HttpFields headers = response.getHeaders();
ImmutableListMultimap.Builder builder = ImmutableListMultimap.builder();
for (String name : headers.getFieldNamesCollection()) {
for (String value : headers.getValuesList(name)) {
builder.put(name, value);
}
}
return builder.build();
}
@Override
public long getBytesRead()
{
return inputStream.getCount();
}
@Override
public InputStream getInputStream()
{
return inputStream;
}
@Override
public String toString()
{
return Objects.toStringHelper(this)
.add("statusCode", getStatusCode())
.add("statusMessage", getStatusMessage())
.add("headers", getHeaders())
.toString();
}
}
private static class JettyResponseFuture
extends AbstractFuture
implements HttpResponseFuture
{
public enum JettyAsyncHttpState
{
WAITING_FOR_CONNECTION,
SENDING_REQUEST,
WAITING_FOR_RESPONSE,
PROCESSING_RESPONSE,
DONE,
FAILED,
CANCELED
}
private static final Logger log = Logger.get(JettyResponseFuture.class);
private final long requestStart = System.nanoTime();
private final AtomicReference state = new AtomicReference<>(JettyAsyncHttpState.WAITING_FOR_CONNECTION);
private final Request request;
private final org.eclipse.jetty.client.api.Request jettyRequest;
private final ResponseHandler responseHandler;
private final AtomicLong bytesWritten;
private final RequestStats stats;
private final String traceToken;
JettyResponseFuture(Request request, org.eclipse.jetty.client.api.Request jettyRequest, ResponseHandler responseHandler, AtomicLong bytesWritten, RequestStats stats)
{
this.request = request;
this.jettyRequest = jettyRequest;
this.responseHandler = responseHandler;
this.bytesWritten = bytesWritten;
this.stats = stats;
traceToken = getCurrentRequestToken();
}
@Override
public String getState()
{
return state.get().toString();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
try {
state.set(JettyAsyncHttpState.CANCELED);
jettyRequest.abort(new CancellationException());
return super.cancel(mayInterruptIfRunning);
}
catch (Throwable e) {
setException(e);
return true;
}
}
protected void completed(Response response, InputStream content)
{
if (state.get() == JettyAsyncHttpState.CANCELED) {
return;
}
T value;
try {
value = processResponse(response, content);
}
catch (Throwable e) {
// this will be an instance of E from the response handler or an Error
storeException(e);
return;
}
state.set(JettyAsyncHttpState.DONE);
set(value);
}
private T processResponse(Response response, InputStream content)
throws E
{
// this time will not include the data fetching portion of the response,
// since the response is fully cached in memory at this point
long responseStart = System.nanoTime();
state.set(JettyAsyncHttpState.PROCESSING_RESPONSE);
JettyResponse jettyResponse = null;
T value;
try (TraceTokenScope ignored = registerRequestToken(traceToken)) {
jettyResponse = new JettyResponse(response, content);
value = responseHandler.handle(request, jettyResponse);
}
finally {
recordRequestComplete(stats, request, requestStart, bytesWritten.get(), jettyResponse, responseStart);
}
return value;
}
protected void failed(Throwable throwable)
{
if (state.get() == JettyAsyncHttpState.CANCELED) {
return;
}
// give handler a chance to rewrite the exception or return a value instead
if (throwable instanceof Exception) {
try (TraceTokenScope ignored = registerRequestToken(traceToken)) {
T value = responseHandler.handleException(request, (Exception) throwable);
// handler returned a value, store it in the future
state.set(JettyAsyncHttpState.DONE);
set(value);
return;
}
catch (Throwable newThrowable) {
throwable = newThrowable;
}
}
// at this point "throwable" will either be an instance of E
// from the response handler or not an instance of Exception
storeException(throwable);
}
private void storeException(Throwable throwable)
{
if (throwable instanceof CancellationException) {
state.set(JettyAsyncHttpState.CANCELED);
}
else {
state.set(JettyAsyncHttpState.FAILED);
}
if (throwable == null) {
throwable = new Throwable("Throwable is null");
log.error(throwable, "Something is broken");
}
setException(throwable);
}
@Override
public String toString()
{
return Objects.toStringHelper(this)
.add("requestStart", requestStart)
.add("state", state)
.add("request", request)
.toString();
}
}
private static void recordRequestComplete(RequestStats requestStats, Request request, long requestStart, long bytesWritten, JettyResponse response, long responseStart)
{
if (response == null) {
return;
}
Duration responseProcessingTime = Duration.nanosSince(responseStart);
Duration requestProcessingTime = new Duration(responseStart - requestStart, NANOSECONDS);
requestStats.record(request.getMethod(),
response.getStatusCode(),
bytesWritten,
response.getBytesRead(),
requestProcessingTime,
responseProcessingTime);
}
private static class BodySourceInputStream extends InputStream
{
private final InputStream delegate;
private final AtomicLong bytesWritten;
BodySourceInputStream(InputStreamBodySource bodySource, AtomicLong bytesWritten)
{
delegate = bodySource.getInputStream();
this.bytesWritten = bytesWritten;
}
@Override
public int read()
throws IOException
{
// We guarantee we don't call the int read() method of the delegate.
throw new UnsupportedOperationException();
}
@Override
public int read(byte[] b)
throws IOException
{
int read = delegate.read(b);
if (read > 0) {
bytesWritten.addAndGet(read);
}
return read;
}
@Override
public int read(byte[] b, int off, int len)
throws IOException
{
int read = delegate.read(b, off, len);
if (read > 0) {
bytesWritten.addAndGet(read);
}
return read;
}
@Override
public long skip(long n)
throws IOException
{
return delegate.skip(n);
}
@Override
public int available()
throws IOException
{
return delegate.available();
}
@Override
public void close()
{
// We guarantee we don't call this
throw new UnsupportedOperationException();
}
@Override
public void mark(int readlimit)
{
// We guarantee we don't call this
throw new UnsupportedOperationException();
}
@Override
public void reset()
throws IOException
{
// We guarantee we don't call this
throw new UnsupportedOperationException();
}
@Override
public boolean markSupported()
{
return false;
}
}
private static class DynamicBodySourceContentProvider
implements ContentProvider
{
private static final ByteBuffer DONE = ByteBuffer.allocate(0);
private final DynamicBodySource dynamicBodySource;
private final AtomicLong bytesWritten;
private final String traceToken;
DynamicBodySourceContentProvider(DynamicBodySource dynamicBodySource, AtomicLong bytesWritten)
{
this.dynamicBodySource = dynamicBodySource;
this.bytesWritten = bytesWritten;
traceToken = getCurrentRequestToken();
}
@Override
public long getLength()
{
return -1;
}
@Override
public Iterator iterator()
{
final Queue chunks = new ArrayQueue<>(4, 64);
Writer writer;
try (TraceTokenScope ignored = registerRequestToken(traceToken)) {
writer = dynamicBodySource.start(new DynamicBodySourceOutputStream(chunks));
}
catch (Exception e) {
throw propagate(e);
}
return new DynamicBodySourceIterator(chunks, writer, bytesWritten, traceToken);
}
private static class DynamicBodySourceOutputStream
extends OutputStream
{
private final Queue chunks;
private DynamicBodySourceOutputStream(Queue chunks)
{
this.chunks = chunks;
}
@Override
public void write(int b)
{
// must copy array since it could be reused
chunks.add(ByteBuffer.wrap(new byte[]{(byte) b}));
}
@Override
public void write(byte[] b, int off, int len)
{
// must copy array since it could be reused
byte[] copy = Arrays.copyOfRange(b, off, len);
chunks.add(ByteBuffer.wrap(copy));
}
@Override
public void close()
{
chunks.add(DONE);
}
}
private static class DynamicBodySourceIterator extends AbstractIterator
implements Closeable
{
private final Queue chunks;
private final Writer writer;
private final AtomicLong bytesWritten;
private final String traceToken;
@SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
DynamicBodySourceIterator(Queue chunks, Writer writer, AtomicLong bytesWritten, String traceToken)
{
this.chunks = chunks;
this.writer = writer;
this.bytesWritten = bytesWritten;
this.traceToken = traceToken;
}
@Override
protected ByteBuffer computeNext()
{
ByteBuffer chunk = chunks.poll();
while (chunk == null) {
try (TraceTokenScope ignored = registerRequestToken(traceToken)) {
writer.write();
}
catch (Exception e) {
throw propagate(e);
}
chunk = chunks.poll();
}
if (chunk == DONE) {
return endOfData();
}
bytesWritten.addAndGet(chunk.array().length);
return chunk;
}
@Override
public void close()
{
if (writer instanceof AutoCloseable) {
try (TraceTokenScope ignored = registerRequestToken(traceToken)) {
((AutoCloseable)writer).close();
}
catch (Exception e) {
throw propagate(e);
}
}
}
}
}
private static class BufferingResponseListener
extends Listener.Adapter
{
private final JettyResponseFuture, ?> future;
private final int maxLength;
@GuardedBy("this")
private byte[] buffer = new byte[0];
@GuardedBy("this")
private int size;
BufferingResponseListener(JettyResponseFuture, ?> future, int maxLength)
{
this.future = checkNotNull(future, "future is null");
Preconditions.checkArgument(maxLength > 0, "maxLength must be greater than zero");
this.maxLength = maxLength;
}
@Override
public synchronized void onHeaders(Response response)
{
long length = response.getHeaders().getLongField(HttpHeader.CONTENT_LENGTH.asString());
if (length > maxLength) {
response.abort(new ResponseTooLargeException());
}
if (length > buffer.length) {
buffer = Arrays.copyOf(buffer, Ints.saturatedCast(length));
}
}
@Override
public synchronized void onContent(Response response, ByteBuffer content)
{
int length = content.remaining();
int requiredCapacity = size + length;
if (requiredCapacity > buffer.length) {
if (requiredCapacity > maxLength) {
response.abort(new ResponseTooLargeException());
return;
}
// newCapacity = min(log2ceiling(requiredCapacity), maxLength);
int newCapacity = min(Integer.highestOneBit(requiredCapacity) << 1, maxLength);
buffer = Arrays.copyOf(buffer, newCapacity);
}
content.get(buffer, size, length);
size += length;
}
@Override
public synchronized void onComplete(Result result)
{
Throwable throwable = result.getFailure();
if (throwable != null) {
future.failed(throwable);
}
else {
future.completed(result.getResponse(), new ByteArrayInputStream(buffer, 0, size));
}
}
}
/*
* This class is needed because jmxutils only fetches a nested instance object once and holds on to it forever.
* todo remove this when https://github.com/martint/jmxutils/issues/26 is implemented
*/
@ThreadSafe
public static class CachedDistribution
{
private final Supplier distributionSupplier;
@GuardedBy("this")
private Distribution distribution;
@GuardedBy("this")
private long lastUpdate = System.nanoTime();
public CachedDistribution(Supplier distributionSupplier)
{
this.distributionSupplier = distributionSupplier;
}
public synchronized Distribution getDistribution()
{
// refresh stats only once a second
if (NANOSECONDS.toMillis(System.nanoTime() - lastUpdate) > 1000) {
this.distribution = distributionSupplier.get();
this.lastUpdate = System.nanoTime();
}
return distribution;
}
@Managed
public double getMaxError()
{
return getDistribution().getMaxError();
}
@Managed
public double getCount()
{
return getDistribution().getCount();
}
@Managed
public double getTotal()
{
return getDistribution().getTotal();
}
@Managed
public long getP01()
{
return getDistribution().getP01();
}
@Managed
public long getP05()
{
return getDistribution().getP05();
}
@Managed
public long getP10()
{
return getDistribution().getP10();
}
@Managed
public long getP25()
{
return getDistribution().getP25();
}
@Managed
public long getP50()
{
return getDistribution().getP50();
}
@Managed
public long getP75()
{
return getDistribution().getP75();
}
@Managed
public long getP90()
{
return getDistribution().getP90();
}
@Managed
public long getP95()
{
return getDistribution().getP95();
}
@Managed
public long getP99()
{
return getDistribution().getP99();
}
@Managed
public long getMin()
{
return getDistribution().getMin();
}
@Managed
public long getMax()
{
return getDistribution().getMax();
}
@Managed
public Map getPercentiles()
{
return getDistribution().getPercentiles();
}
}
private static class JettyRequestListener
{
enum State
{
CREATED, SENDING_REQUEST, AWAITING_RESPONSE, READING_RESPONSE, FINISHED
}
private final AtomicReference state = new AtomicReference<>(State.CREATED);
private final URI uri;
private final long created = System.nanoTime();
private final AtomicLong requestStarted = new AtomicLong();
private final AtomicLong requestFinished = new AtomicLong();
private final AtomicLong responseStarted = new AtomicLong();
private final AtomicLong responseFinished = new AtomicLong();
JettyRequestListener(URI uri)
{
this.uri = uri;
}
public URI getUri()
{
return uri;
}
public State getState()
{
return state.get();
}
public long getCreated()
{
return created;
}
public long getRequestStarted()
{
return requestStarted.get();
}
public long getRequestFinished()
{
return requestFinished.get();
}
public long getResponseStarted()
{
return responseStarted.get();
}
public long getResponseFinished()
{
return responseFinished.get();
}
public void onRequestBegin()
{
changeState(State.SENDING_REQUEST);
long now = System.nanoTime();
requestStarted.compareAndSet(0, now);
}
public void onRequestEnd()
{
changeState(State.AWAITING_RESPONSE);
long now = System.nanoTime();
requestStarted.compareAndSet(0, now);
requestFinished.compareAndSet(0, now);
}
private void onResponseBegin()
{
changeState(State.READING_RESPONSE);
long now = System.nanoTime();
requestStarted.compareAndSet(0, now);
requestFinished.compareAndSet(0, now);
responseStarted.compareAndSet(0, now);
}
private void onFinish()
{
changeState(State.FINISHED);
long now = System.nanoTime();
requestStarted.compareAndSet(0, now);
requestFinished.compareAndSet(0, now);
responseStarted.compareAndSet(0, now);
responseFinished.compareAndSet(0, now);
}
private synchronized void changeState(State newState)
{
if (state.get().ordinal() < newState.ordinal()) {
state.set(newState);
}
}
}
private static class ConnectionPoolDistribution
extends CachedDistribution
{
interface Processor
{
void process(Distribution distribution, DuplexConnectionPool pool);
}
ConnectionPoolDistribution(HttpClient httpClient, Processor processor)
{
super(() -> {
Distribution distribution = new Distribution();
httpClient.getDestinations().stream()
.filter(PoolingHttpDestination.class::isInstance)
.map(destination -> (PoolingHttpDestination>) destination)
.map(PoolingHttpDestination::getConnectionPool)
.filter(pool -> pool != null)
.forEach(pool -> processor.process(distribution, pool));
return distribution;
});
}
}
private static class DestinationDistribution
extends CachedDistribution
{
interface Processor
{
void process(Distribution distribution, PoolingHttpDestination> destination);
}
DestinationDistribution(HttpClient httpClient, Processor processor)
{
super(() -> {
Distribution distribution = new Distribution();
httpClient.getDestinations().stream()
.filter(PoolingHttpDestination.class::isInstance)
.map(destination -> (PoolingHttpDestination>) destination)
.forEach(destination -> processor.process(distribution, destination));
return distribution;
});
}
}
private static class RequestDistribution
extends CachedDistribution
{
interface Processor
{
void process(Distribution distribution, JettyRequestListener listener, long now);
}
RequestDistribution(HttpClient httpClient, Processor processor)
{
super(() -> {
long now = System.nanoTime();
Distribution distribution = new Distribution();
httpClient.getDestinations().stream()
.filter(PoolingHttpDestination.class::isInstance)
.map(destination -> (PoolingHttpDestination>) destination)
.map(JettyHttpClient::getRequestListenersForDestination)
.flatMap(List::stream)
.forEach(listener -> processor.process(distribution, listener, now));
return distribution;
});
}
}
private static class LimitQueuedToAvailableConnectionsHttpDestination
extends HttpDestinationOverHTTP
{
private final Object lock = new Object();
private final int limit;
LimitQueuedToAvailableConnectionsHttpDestination(int limit, HttpClient httpClient, Origin origin)
{
super(httpClient, origin);
this.limit = limit;
}
@Override
protected boolean enqueue(Queue queue, HttpExchange exchange)
{
synchronized (lock) {
int connectionCount = getConnectionPool().getActiveConnections().size();
int size = queue.size();
if (size + connectionCount >= limit)
{
return false;
}
return super.enqueue(queue, exchange);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy