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

org.jboss.resteasy.client.jaxrs.engines.ApacheHttpAsyncClient4Engine Maven / Gradle / Ivy

There is a newer version: 7.0.0.Alpha4
Show newest version
package org.jboss.resteasy.client.jaxrs.engines;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.function.Supplier;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;

import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.InvocationCallback;
import jakarta.ws.rs.client.ResponseProcessingException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;

import org.apache.http.ContentTooLongException;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.concurrent.BasicFuture;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.nio.ContentDecoder;
import org.apache.http.nio.IOControl;
import org.apache.http.nio.client.methods.HttpAsyncMethods;
import org.apache.http.nio.entity.ContentInputStream;
import org.apache.http.nio.protocol.AbstractAsyncResponseConsumer;
import org.apache.http.nio.protocol.HttpAsyncRequestProducer;
import org.apache.http.nio.protocol.HttpAsyncResponseConsumer;
import org.apache.http.nio.util.HeapByteBufferAllocator;
import org.apache.http.nio.util.SharedInputBuffer;
import org.apache.http.nio.util.SimpleInputBuffer;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.jboss.resteasy.client.jaxrs.i18n.LogMessages;
import org.jboss.resteasy.client.jaxrs.internal.ClientConfiguration;
import org.jboss.resteasy.client.jaxrs.internal.ClientInvocation;
import org.jboss.resteasy.client.jaxrs.internal.ClientResponse;
import org.jboss.resteasy.client.jaxrs.internal.FinalizedClientResponse;
import org.jboss.resteasy.tracing.RESTEasyTracingLogger;
import org.jboss.resteasy.util.CaseInsensitiveMap;

/**
 * AsyncClientHttpEngine using apache http components HttpAsyncClient 4.
 * 

* * Some words of caution: *

    *
  • Asynchronous IO means non-blocking IO utilizing few threads, typically at most as much threads as number of cores. * As such, performance may profit from fewer thread switches and less memory usage due to fewer thread-stacks. But doing * synchronous, blocking IO (the invoke-methods not returning a future) may suffer, because the data has to be transferred * piecewiese to/from the io-threads.
  • *
  • Request-Entities are fully buffered in memory, thus this engine is unsuitable for very large uploads.
  • *
  • Response-Entities are buffered in memory, except if requesting a Response, InputStream or Reader as Result. Thus * for large downloads or COMET one of these three return types must be requested, but there may be a performance penalty * because the response-body is transferred piecewise from the io-threads. When using InvocationCallbacks, the response is * always fully buffered in memory.
  • *
  • InvocationCallbacks are called from within the io-threads and thus must not block or else the application may * slow down to a halt. Reading the response is safe (because the response is buffered in memory), as are other async * (and in-memory) Client-invocations (the submit-calls returning a future not containing Response, InputStream or Reader). * Again, there must be no blocking IO inside InvocationCallback! (If you are wondering why not to allow blocking calls by * wrapping InvocationCallbacks in extra threads: Because then the main advantage of async IO, less threading, is lost.) *
  • InvocationCallbacks may be called seemingly "after" the future-object returns. Thus, responses should be handled * solely in the InvocationCallback.
  • *
  • InvocationCallbacks will see the same result as the future-object and vice versa. Thus, if the invocationcallback * throws an exception, the future-object will not see it. Another reason to handle responses only in the InvocationCallback. *
  • *
* * @author Markus Kull */ public class ApacheHttpAsyncClient4Engine implements AsyncClientHttpEngine, Closeable { protected final CloseableHttpAsyncClient client; protected final boolean closeHttpClient; public ApacheHttpAsyncClient4Engine(final CloseableHttpAsyncClient client, final boolean closeHttpClient) { if (client == null) throw new NullPointerException("client"); this.client = client; this.closeHttpClient = closeHttpClient; if (closeHttpClient && !client.isRunning()) { client.start(); } } @Override public void close() { if (closeHttpClient) { safeClose(client); } } @Override public SSLContext getSslContext() { throw new UnsupportedOperationException(); } @Override public HostnameVerifier getHostnameVerifier() { throw new UnsupportedOperationException(); } @Override public Response invoke(Invocation request) { // Doing blocking requests with an async httpclient is quite useless. // But it is better to use the same httpclient in any case just for sharing+configuring only one connectionpool. Future future = submit((ClientInvocation) request, false, null, new ResultExtractor() { @Override public ClientResponse extractResult(ClientResponse response) { return response; } }); try { return future.get(); } catch (InterruptedException e) { future.cancel(true); throw clientException(e, null); } catch (ExecutionException e) { throw clientException(e.getCause(), null); } } @Override public Future submit( ClientInvocation request, boolean buffered, InvocationCallback callback, ResultExtractor extractor) { HttpUriRequest httpRequest = buildHttpRequest(request); if (buffered) { // Request+Response fully buffered in memory. Optional callback is called inside io-thread after response-body and // after the returned future is signaled to be completed. // // This differs to Resteasy 3.0.8 and earlier (which called the callback before the future completed) due to the // following reasons: // * ApacheHttpcomponents BasicFuture, guavas ListenableFuture and also jersey calls the callback after completing // the future. The earlier Resteasy-behaviour may be more "safe" but any users switching from resteasy to another // jax-rs implementation may encounter a nasty surprise. // * ensure the result returned by the future is the same given to the callback. // * As good practice, the result should only be handled in one place (future OR callback, not both) // * Invocation-javadoc says "the underlying response instance will be automatically closed" seemingly implying // the future-response is unusable (bc. closed) together with a callback // Of course the one big drawback is that exceptions inside the callback are not visible to the application, // but callbacks are mostly treated as fire-and-forget, meaning their result is not checked anyway. HttpAsyncRequestProducer requestProducer = HttpAsyncMethods.create(httpRequest); HttpAsyncResponseConsumer responseConsumer = new BufferingResponseConsumer(request, extractor); FutureCallback httpCallback = callback != null ? new CallbackAdapter(callback) : null; return client.execute(requestProducer, responseConsumer, httpCallback); } else { // unbuffered: Future returns immediately after headers. Reading the response-stream blocks, but one may check // InputStream#available() to prevent blocking. // would be easy to call an InvocationCallback after response-BODY, but cant see any usecase for it. if (callback != null) throw new IllegalArgumentException("unbuffered InvocationCallback is not supported"); HttpAsyncRequestProducer requestProducer = HttpAsyncMethods.create(httpRequest); StreamingResponseConsumer responseConsumer = new StreamingResponseConsumer(request, extractor); Future httpFuture = client.execute(requestProducer, responseConsumer, null); return responseConsumer.future(httpFuture); } } @Override public CompletableFuture submit(ClientInvocation request, boolean buffered, ResultExtractor extractor, ExecutorService executorService) { if (buffered) { final CompletableFuture cf = new CompletableFuture<>(); final InvocationCallback callback = new InvocationCallback() { @Override public void completed(T response) { cf.complete(response); } @Override public void failed(Throwable throwable) { cf.completeExceptionally(throwable); } }; submit(request, buffered, callback, extractor); return cf; } else { final Supplier supplier = () -> { try { return submit(request, buffered, null, extractor).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } }; if (executorService == null) { return CompletableFuture.supplyAsync(supplier); } else { return CompletableFuture.supplyAsync(supplier, executorService); } } } /** * ResponseConsumer which transfers the response piecewise from the io-thread to the blocking handler-thread. * {@link #future(Future)} returns a Future which completes immediately after receiving the response-headers * but reading the response-inputstream blocks until data is available. */ private static class StreamingResponseConsumer implements HttpAsyncResponseConsumer { @SuppressWarnings("serial") private static final IOException unallowedBlockingReadException = new IOException( "blocking reads inside an async io-handler are not allowed") { public synchronized Throwable fillInStackTrace() { //do nothing and return return this; } }; private ClientConfiguration configuration; private Map properties; private ResultExtractor extractor; private ResultFuture future; private SharedInputStream sharedStream; private boolean hasResult; private volatile T result; private volatile Exception exception; private volatile boolean completed; StreamingResponseConsumer(final ClientInvocation request, final ResultExtractor extractor) { this.configuration = request.getClientConfiguration(); this.properties = request.getMutableProperties(); this.extractor = extractor; } private void releaseResources() { this.configuration = null; this.properties = null; this.extractor = null; this.future = null; this.sharedStream = null; } public synchronized Future future(Future httpFuture) { if (completed) { // already failed or fully buffered return httpFuture; } future = new ResultFuture(httpFuture); future.copyHttpFutureResult(); if (!future.isDone() && hasResult) { // response(-headers) is available, but not yet the full response-stream. Return immediately the result future.completed(getResult()); } return future; } @Override public synchronized void responseReceived(HttpResponse httpResponse) throws IOException, HttpException { SharedInputStream sharedStream = null; ConnectionResponse clientResponse = null; T result = null; Exception exception = null; boolean success = false; try { clientResponse = new ConnectionResponse(configuration, properties); copyResponse(httpResponse, clientResponse); final HttpEntity entity = httpResponse.getEntity(); if (entity != null) { sharedStream = new SharedInputStream(new SharedInputBuffer(16 * 1024)); // one could also set the stream after extracting the response, but this would prevent wrapping the stream clientResponse.setConnection(sharedStream); sharedStream.setException(unallowedBlockingReadException); result = extractor.extractResult(clientResponse); sharedStream.setException(null); } else { result = extractor.extractResult(clientResponse); } success = true; } catch (Exception e) { exception = clientException(e, clientResponse); } finally { if (success) { this.sharedStream = sharedStream; this.result = result; this.hasResult = true; if (future != null) future.completed(result); } else { this.exception = exception; completed = true; if (future != null) future.failed(exception); releaseResources(); } } } @Override public synchronized void consumeContent(ContentDecoder decoder, IOControl ioctrl) throws IOException { if (sharedStream != null) sharedStream.consumeContent(decoder, ioctrl); } @Override public synchronized void responseCompleted(HttpContext context) { this.completed = true; try { if (sharedStream != null) { // only needed in case of empty response body (=null ioctrl) sharedStream.consumeContent(EndOfStream.INSTANCE, null); } } catch (IOException ioe) { // cannot happen throw new RuntimeException(ioe); } finally { releaseResources(); } } @Override public Exception getException() { return exception; } @Override public T getResult() { return result; } @Override public boolean isDone() { // cancels in case of closing the SharedInputStream return completed; } @Override public synchronized void close() { completed = true; ResultFuture future = this.future; if (future != null) { // if connect fails, then the httpclient just calls close() after setting its future, but never our failed(). // so copy the httpFuture-result into our ResultFuture. future.copyHttpFutureResult(); if (!future.isDone()) { // doesnt happen? future.failed(clientException(new IOException("connect failed"), null)); } } releaseResources(); } @Override public synchronized void failed(Exception ex) { completed = true; if (future != null) future.failed(clientException(ex, null)); if (sharedStream != null) { sharedStream.setException(ioException(ex)); safeClose(sharedStream); } releaseResources(); } @Override public synchronized boolean cancel() { completed = true; if (future != null) future.cancelledResult(); if (sharedStream != null) { sharedStream.setException(new IOException("cancelled")); safeClose(sharedStream); } releaseResources(); return true; } private static class ResultFuture extends BasicFuture { private final Future httpFuture; ResultFuture(final Future httpFuture) { super(null); this.httpFuture = httpFuture; } @Override public boolean cancel(boolean mayInterruptIfRunning) { boolean cancelled = super.cancel(mayInterruptIfRunning); httpFuture.cancel(mayInterruptIfRunning); return cancelled; } public void cancelledResult() { super.cancel(true); } public void copyHttpFutureResult() { if (!isDone() && httpFuture.isDone()) { try { completed(httpFuture.get()); } catch (ExecutionException e) { failed(clientException(e.getCause(), null)); } catch (InterruptedException e) { // cant happen because already isDone failed(e); } } } } private class SharedInputStream extends ContentInputStream { private final SharedInputBuffer sharedBuf; private volatile IOException ex; private volatile IOControl ioctrl; SharedInputStream(final SharedInputBuffer sharedBuf) { super(sharedBuf); this.sharedBuf = sharedBuf; } public void consumeContent(ContentDecoder decoder, IOControl ioctrl) throws IOException { if (ioctrl != null) this.ioctrl = ioctrl; sharedBuf.consumeContent(decoder, ioctrl); } @Override public void close() throws IOException { completed = true; // next isDone() cancels. // Workaround for deadlock: super.close() reads until no more data, but on cancellation no more data is // pushed to consumeContent, thus deadlock. Instead notify the reactor by ioctrl.requestInput sharedBuf.close(); // next reads will return EndOfStream. Also wakes up any waiting readers IOControl ioctrl = this.ioctrl; if (ioctrl != null) ioctrl.requestInput(); // notify reactor to check isDone() super.close(); // does basically nothing due to closed buf } @Override public int read(final byte[] b, final int off, final int len) throws IOException { throwIfError(); return super.read(b, off, len); } @Override public int read(final byte[] b) throws IOException { throwIfError(); return super.read(b, 0, b.length); } @Override public int read() throws IOException { throwIfError(); return super.read(); } private void throwIfError() throws IOException { IOException ex = this.ex; if (ex != null) { //create a new exception here to make it easy figuring out where the offending blocking IO comes from throw new IOException(ex); } } public void setException(IOException e) { this.ex = e; } } } /** * Buffers response fully in memory. * * (Buffering is definitely easier to implement than streaming) */ private static class BufferingResponseConsumer extends AbstractAsyncResponseConsumer { private ClientConfiguration configuration; private Map properties; private ResultExtractor responseExtractor; private ConnectionResponse clientResponse; private SimpleInputBuffer buf; BufferingResponseConsumer(final ClientInvocation request, final ResultExtractor responseExtractor) { this.configuration = request.getClientConfiguration(); this.properties = request.getMutableProperties(); this.responseExtractor = responseExtractor; } @Override protected void onResponseReceived(HttpResponse response) throws HttpException, IOException { ConnectionResponse clientResponse = new ConnectionResponse(configuration, properties); copyResponse(response, clientResponse); final HttpEntity entity = response.getEntity(); if (entity != null) { long len = entity.getContentLength(); if (len > Integer.MAX_VALUE) { throw new ContentTooLongException("Entity content is too long: " + len); } if (len < 0) { len = 4096; } this.buf = new SimpleInputBuffer((int) len, new HeapByteBufferAllocator()); } this.clientResponse = clientResponse; } @Override protected void onEntityEnclosed(HttpEntity entity, ContentType contentType) throws IOException { } @Override protected void onContentReceived(ContentDecoder decoder, IOControl ioctrl) throws IOException { SimpleInputBuffer buf = this.buf; if (buf == null) throw new NullPointerException("Content Buffer"); buf.consumeContent(decoder); } @Override protected T buildResult(HttpContext context) throws Exception { if (buf != null) clientResponse.setConnection(new ContentInputStream(buf)); return responseExtractor.extractResult(clientResponse); } @Override protected void releaseResources() { this.configuration = null; this.properties = null; this.responseExtractor = null; this.clientResponse = null; this.buf = null; } } /** * Adapter from http-FutureCallback to InvocationCallback */ private static class CallbackAdapter implements FutureCallback { private final InvocationCallback invocationCallback; CallbackAdapter(final InvocationCallback invocationCallback) { this.invocationCallback = invocationCallback; } @Override public void cancelled() { invocationCallback.failed(new ProcessingException("cancelled")); } @Override public void completed(T response) { try { invocationCallback.completed(response); } catch (Throwable t) { LogMessages.LOGGER.exceptionIgnored(t); } finally { // just to promote proper callback usage, because HttpAsyncClient is responsible // for cleaning up the (buffered) connection if (response instanceof Response) { ((Response) response).close(); } } } @Override public void failed(Exception ex) { invocationCallback.failed(clientException(ex, null)); } } /** * ClientResponse with surefire releaseConnection */ private static class ConnectionResponse extends FinalizedClientResponse { private InputStream connection; private InputStream stream; ConnectionResponse(final ClientConfiguration configuration, final Map properties) { super(configuration, RESTEasyTracingLogger.empty()); setProperties(properties); } public synchronized void setConnection(InputStream connection) { this.connection = connection; this.stream = connection; } @Override protected synchronized void setInputStream(InputStream is) { stream = is; resetEntity(); } @Override public synchronized InputStream getInputStream() { return stream; } @Override public synchronized void releaseConnection() throws IOException { releaseConnection(false); } @Override public synchronized void releaseConnection(boolean consumeInputStream) throws IOException { boolean thrown = true; try { if (stream != null) { if (consumeInputStream) { while (stream.read() > 0) { } } stream.close(); } thrown = false; } finally { if (connection != null) { if (thrown) { safeClose(connection); } else { connection.close(); } } } } } private static class EndOfStream implements ContentDecoder { public static EndOfStream INSTANCE = new EndOfStream(); @Override public int read(ByteBuffer dst) throws IOException { return -1; } @Override public boolean isCompleted() { return true; } } private static HttpUriRequest buildHttpRequest(ClientInvocation request) { // Writers may change headers. Thus buffer the content before committing the headers. // For simplicity's sake the content is buffered in memory. File-buffering (ZeroCopyConsumer...) would be // possible, but resource management is error-prone. HttpRequestBase httpRequest = createHttpMethod(request.getUri(), request.getMethod()); if (request.getEntity() != null) { byte[] requestContent = requestContent(request); ByteArrayEntity entity = new ByteArrayEntity(requestContent); final MediaType mediaType = request.getHeaders().getMediaType(); if (mediaType != null) { entity.setContentType(new BasicHeader(HTTP.CONTENT_TYPE, mediaType.toString())); } commitHeaders(request, httpRequest); ((HttpEntityEnclosingRequest) httpRequest).setEntity(entity); } else { commitHeaders(request, httpRequest); } return httpRequest; } private static byte[] requestContent(ClientInvocation request) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); request.getDelegatingOutputStream().setDelegate(baos); try { request.writeRequestBody(request.getEntityStream()); baos.close(); return baos.toByteArray(); } catch (IOException e) { throw new RuntimeException(e); } } private static HttpRequestBase createHttpMethod(URI url, String restVerb) { if ("GET".equals(restVerb)) { return new HttpGet(url); } else if ("POST".equals(restVerb)) { return new HttpPost(url); } else { final String verb = restVerb; return new HttpPost(url) { @Override public String getMethod() { return verb; } }; } } private static void commitHeaders(ClientInvocation request, HttpRequestBase httpMethod) { MultivaluedMap headers = request.getHeaders().asMap(); for (Map.Entry> header : headers.entrySet()) { List values = header.getValue(); for (String value : values) { httpMethod.addHeader(header.getKey(), value); } } } private static void copyResponse(HttpResponse httpResponse, ClientResponse clientResponse) { clientResponse.setStatus(httpResponse.getStatusLine().getStatusCode()); CaseInsensitiveMap headers = new CaseInsensitiveMap(); for (Header header : httpResponse.getAllHeaders()) { headers.add(header.getName(), header.getValue()); } clientResponse.setHeaders(headers); } private static RuntimeException clientException(Throwable ex, Response clientResponse) { RuntimeException ret; if (ex == null) { ret = new ProcessingException(new NullPointerException()); } else if (ex instanceof WebApplicationException) { ret = (WebApplicationException) ex; } else if (ex instanceof ProcessingException) { ret = (ProcessingException) ex; } else if (clientResponse != null) { ret = new ResponseProcessingException(clientResponse, ex); } else { ret = new ProcessingException(ex); } return ret; } private static IOException ioException(Exception ex) { return (ex instanceof IOException) ? (IOException) ex : new IOException(ex); } private static void safeClose(final Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (IOException ignore) { } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy