All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.mklinger.qetcher.client.httpclient.internal.jetty.JettyHttpClient Maven / Gradle / Ivy

There is a newer version: 2.0.42.rc
Show newest version
package de.mklinger.commons.httpclient.internal.jetty;

import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Iterator;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response.CompleteListener;
import org.eclipse.jetty.client.util.DeferredContentProvider;
import org.eclipse.jetty.http2.HTTP2Session;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.Container;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.mklinger.commons.httpclient.HttpClient;
import de.mklinger.commons.httpclient.HttpRequest;
import de.mklinger.commons.httpclient.HttpRequest.BodyProvider;
import de.mklinger.commons.httpclient.HttpResponse;
import de.mklinger.commons.httpclient.HttpResponse.BodyHandler;

/**
 * @author Marc Klinger - mklinger[at]mklinger[dot]de
 */
public class JettyHttpClient implements HttpClient {
	private static final Logger LOG = LoggerFactory.getLogger(JettyHttpClient.class);

	private final org.eclipse.jetty.client.HttpClient jettyClient;
	private volatile boolean closed = false;

	public JettyHttpClient(final org.eclipse.jetty.client.HttpClient jettyClient) {
		this.jettyClient = jettyClient;
		this.jettyClient.addEventListener(new SessionCountListener());
	}

	// For gauge metrics
	private static final AtomicLong openSessions = new AtomicLong();

	private org.eclipse.jetty.client.HttpClient getJettyClient() {
		if (closed) {
			throw new IllegalStateException("Closed");
		}
		return jettyClient;
	}

	private final class SessionCountListener implements Container.InheritedListener {
		@Override
		public void beanAdded(final Container parent, final Object child) {
			if (child instanceof HTTP2Session) {
				final HTTP2Session session = (HTTP2Session) child;
				LOG.debug("Opened HTTP/2 session: {}", session.getEndPoint().getRemoteAddress());
				openSessions.incrementAndGet();
			}
		}

		@Override
		public void beanRemoved(final Container parent, final Object child) {
			if (child instanceof HTTP2Session) {
				final HTTP2Session session = (HTTP2Session) child;
				LOG.debug("Closed HTTP/2 session: {}", session.getEndPoint().getRemoteAddress());
				openSessions.decrementAndGet();
			}
		}
	}

	@Override
	public  CompletableFuture> sendAsync(final HttpRequest request, final BodyHandler responseBodyHandler) {
		try {

			final Request jettyRequest = getJettyClient().newRequest(request.uri())
					.method(request.method());

			applyTimeout(request, jettyRequest);
			applyHeaders(request, jettyRequest);
			applyBody(request, jettyRequest);

			//final Executor completionExecutor = getJettyClient().getExecutor();
			final Executor completionExecutor = ForkJoinPool.commonPool();
			final FullCompleteListener fullCompleteListener = new FullCompleteListener<>(completionExecutor, responseBodyHandler);

			final CompleteListener possibleTimeoutCompleteListener = applyTimeout(request, jettyRequest, fullCompleteListener);

			LOG.debug("Sending jetty request");
			jettyRequest.send(possibleTimeoutCompleteListener);

			return fullCompleteListener.getResult()
					.thenApply(this::toHttpResponse);

		} catch (final Throwable e) {
			// TODO is this a good pattern? Better directly throw?
			final CompletableFuture> errorResult = new CompletableFuture<>();
			errorResult.completeExceptionally(e);
			return errorResult;
		}
	}

	private void applyTimeout(final HttpRequest request, final Request jettyRequest) {
		final Optional timeout = request.timeout();
		if (!timeout.isPresent()) {
			return;
		}

		// Jetty client impl uses millis internally. No need for more precision here.
		// See org.eclipse.jetty.client.HttpRequest.timeout(long, TimeUnit)
		try {
			jettyRequest.timeout(timeout.get().toMillis(), TimeUnit.MILLISECONDS);
		} catch (final ArithmeticException ex) {
			jettyRequest.timeout(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
		}
	}

	private  CompleteListener applyTimeout(final HttpRequest request, final Request jettyRequest, final FullCompleteListener fullCompleteListener) {
		if (request.timeout().isPresent()) {
			// TODO underlying implementation supports more than millis
			return new TimeoutResponseListener(
					fullCompleteListener,
					jettyRequest,
					request.timeout().get().toMillis(),
					TimeUnit.MILLISECONDS,
					getJettyClient().getScheduler());
		} else {
			return fullCompleteListener;
		}
	}

	private void applyHeaders(final HttpRequest request, final Request jettyRequest) {
		request.headers().map().forEach(
				(name, values) -> values.forEach(
						value -> jettyRequest.header(name, value)));
	}

	private void applyBody(final HttpRequest request, final Request jettyRequest) {
		final Optional optionalBodyProvider = request.bodyProvider();
		if (!optionalBodyProvider.isPresent()) {
			return;
		}

		final BodyProvider bodyProvider = optionalBodyProvider.get();

		final long contentLength = getContentLength(bodyProvider);
		if (contentLength == 0) {
			return;
		}

		final Optional bodyProviderContentType = bodyProvider.contentType();
		if (bodyProviderContentType.isPresent() && jettyRequest.getHeaders().get("Content-Type") == null) {
			jettyRequest.header("Content-Type", bodyProviderContentType.get());
		}

		final DeferredContentProvider deferredContentProvider = new DeferredContentProvider() {
			@Override
			public long getLength() {
				return contentLength;
			}
		};

		final RequestBodyFiller requestBodyFiller = new RequestBodyFiller(bodyProvider.iterator(), deferredContentProvider, jettyRequest);
		requestBodyFiller.start();

		jettyRequest.content(deferredContentProvider);
	}

	private static class RequestBodyFiller {
		private final Iterator> chunkFutureIterator;
		private final DeferredContentProvider deferredContentProvider;
		private final Request jettyRequest;
		private final AtomicReference error = new AtomicReference<>();

		private final Object pendingLock = new Object();
		private long pendingOffers = 0L;
		private boolean pendingRead = false;

		private final static long MAX_PENDING_OFFERS = 5;

		private final Callback offerCallback = new Callback() {
			@Override
			public void succeeded() {
				synchronized (pendingLock) {
					pendingOffers--;
					fillIfPossible();
				}
			}

			@Override
			public void failed(final Throwable e) {
				synchronized (pendingLock) {
					pendingOffers--;
				}
				error(e);
			}
		};

		public RequestBodyFiller(final Iterator> chunkFutureIterator, final DeferredContentProvider deferredContentProvider, final Request jettyRequest) {
			this.chunkFutureIterator = chunkFutureIterator;
			this.deferredContentProvider = deferredContentProvider;
			this.jettyRequest = jettyRequest;
		}

		private void fillIfPossible() {
			if (!Thread.holdsLock(pendingLock)) {
				throw new IllegalStateException();
			}
			if (pendingOffers < MAX_PENDING_OFFERS && !pendingRead) {
				LOG.debug("Filling with {} pending offers", pendingOffers);
				fill();
			}
		}

		public void start() {
			synchronized (pendingLock) {
				fill();
			}
		}

		private void fill() {
			if (isError()) {
				return;
			}
			try {

				if (!Thread.holdsLock(pendingLock)) {
					throw new IllegalStateException();
				}
				if (pendingRead) {
					throw new IllegalStateException();
				}
				pendingRead = true;

				if (chunkFutureIterator.hasNext()) {
					chunkFutureIterator.next().whenComplete(this::chunkFutureComplete);
				} else {
					done();
				}
			} catch (final Throwable e) {
				error(e);
			}
		}


		private void chunkFutureComplete(final ByteBuffer byteBuffer, final Throwable error) {
			if (error != null) {
				error(error);
				return;
			}

			final boolean success = deferredContentProvider.offer(byteBuffer, offerCallback);
			if (!success) {
				error(new RuntimeException("Failed to offer content to deferred content provider"));
			}

			synchronized (pendingLock) {
				pendingOffers++;
				pendingRead = false;
				fillIfPossible();
			}
		}

		private void done() {
			deferredContentProvider.close();
		}

		public void error(final Throwable error) {
			final boolean set = this.error.compareAndSet(null, error);
			if (!set && this.error.get() != error) {
				this.error.get().addSuppressed(error);
			}
			jettyRequest.abort(this.error.get());
			deferredContentProvider.close();
		}

		public boolean isError() {
			return error.get() != null;
		}
	}

	private long getContentLength(final BodyProvider bodyProvider) {
		final long contentLength = bodyProvider.contentLength();
		if (contentLength >= 0) {
			return contentLength;
		}
		return -1;
	}

	private  HttpResponse toHttpResponse(final BodyResult result) {
		LOG.debug("Building final HttpResponse");
		return new JettyHttpResponse<>(
				result.getResult().getResponse().getStatus(),
				new JettyHttpRequest(result.getResult().getRequest()),
				HeadersTransformation.toHttpHeaders(result.getResult().getResponse().getHeaders()),
				result.getBody());
	}

	@Override
	public void close() {
		closed = true;
		try {
			jettyClient.stop();
		} catch (final Exception e) {
			throw new RuntimeException(e);
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy