Search in sources :

Example 1 with BasicAsyncResponseConsumer

use of org.apache.http.nio.protocol.BasicAsyncResponseConsumer in project nifi by apache.

the class SiteToSiteRestApiClient method initiateTransactionForSend.

/**
 * <p>
 * Initiate a transaction for sending data.
 * </p>
 *
 * <p>
 * If a proxy server requires auth, the proxy server returns 407 response with available auth schema such as basic or digest.
 * Then client has to resend the same request with its credential added.
 * This mechanism is problematic for sending data from NiFi.
 * </p>
 *
 * <p>
 * In order to resend a POST request with auth param,
 * NiFi has to either read flow-file contents to send again, or keep the POST body somewhere.
 * If we store that in memory, it would causes OOM, or storing it on disk slows down performance.
 * Rolling back processing session would be overkill.
 * Reading flow-file contents only when it's ready to send in a streaming way is ideal.
 * </p>
 *
 * <p>
 * Additionally, the way proxy authentication is done is vary among Proxy server software.
 * Some requires 407 and resend cycle for every requests, while others keep a connection between a client and
 * the proxy server, then consecutive requests skip auth steps.
 * The problem is, that how should we behave is only told after sending a request to the proxy.
 * </p>
 *
 * In order to handle above concerns correctly and efficiently, this method do the followings:
 *
 * <ol>
 * <li>Send a GET request to controller resource, to initiate an HttpAsyncClient. The instance will be used for further requests.
 *      This is not required by the Site-to-Site protocol, but it can setup proxy auth state safely.</li>
 * <li>Send a POST request to initiate a transaction. While doing so, it captures how a proxy server works.
 * If 407 and resend cycle occurs here, it implies that we need to do the same thing again when we actually send the data.
 * Because if the proxy keeps using the same connection and doesn't require an auth step, it doesn't do so here.</li>
 * <li>Then this method stores whether the final POST request should wait for the auth step.
 * So that {@link #openConnectionForSend} can determine when to produce contents.</li>
 * </ol>
 *
 * <p>
 * The above special sequence is only executed when a proxy instance is set, and its username is set.
 * </p>
 *
 * @param post a POST request to establish transaction
 * @return POST request response
 * @throws IOException thrown if the post request failed
 */
private HttpResponse initiateTransactionForSend(final HttpPost post) throws IOException {
    if (shouldCheckProxyAuth()) {
        final CloseableHttpAsyncClient asyncClient = getHttpAsyncClient();
        final HttpGet get = createGetControllerRequest();
        final Future<HttpResponse> getResult = asyncClient.execute(get, null);
        try {
            final HttpResponse getResponse = getResult.get(readTimeoutMillis, TimeUnit.MILLISECONDS);
            logger.debug("Proxy auth check has done. getResponse={}", getResponse.getStatusLine());
        } catch (final ExecutionException e) {
            logger.debug("Something has happened at get controller requesting thread for proxy auth check. {}", e.getMessage());
            throw toIOException(e);
        } catch (TimeoutException | InterruptedException e) {
            throw new IOException(e);
        }
    }
    final HttpAsyncRequestProducer asyncRequestProducer = new HttpAsyncRequestProducer() {

        private boolean requestHasBeenReset = false;

        @Override
        public HttpHost getTarget() {
            return URIUtils.extractHost(post.getURI());
        }

        @Override
        public HttpRequest generateRequest() throws IOException, HttpException {
            final BasicHttpEntity entity = new BasicHttpEntity();
            post.setEntity(entity);
            return post;
        }

        @Override
        public void produceContent(ContentEncoder encoder, IOControl ioctrl) throws IOException {
            encoder.complete();
            if (shouldCheckProxyAuth() && requestHasBeenReset) {
                logger.debug("Produced content again, assuming the proxy server requires authentication.");
                proxyAuthRequiresResend.set(true);
            }
        }

        @Override
        public void requestCompleted(HttpContext context) {
            debugProxyAuthState(context);
        }

        @Override
        public void failed(Exception ex) {
            final String msg = String.format("Failed to create transaction for %s", post.getURI());
            logger.error(msg, ex);
            eventReporter.reportEvent(Severity.WARNING, EVENT_CATEGORY, msg);
        }

        @Override
        public boolean isRepeatable() {
            return true;
        }

        @Override
        public void resetRequest() throws IOException {
            requestHasBeenReset = true;
        }

        @Override
        public void close() throws IOException {
        }
    };
    final Future<HttpResponse> responseFuture = getHttpAsyncClient().execute(asyncRequestProducer, new BasicAsyncResponseConsumer(), null);
    final HttpResponse response;
    try {
        response = responseFuture.get(readTimeoutMillis, TimeUnit.MILLISECONDS);
    } catch (final ExecutionException e) {
        logger.debug("Something has happened at initiate transaction requesting thread. {}", e.getMessage());
        throw toIOException(e);
    } catch (TimeoutException | InterruptedException e) {
        throw new IOException(e);
    }
    return response;
}
Also used : ContentEncoder(org.apache.http.nio.ContentEncoder) HttpGet(org.apache.http.client.methods.HttpGet) IOControl(org.apache.http.nio.IOControl) HttpContext(org.apache.http.protocol.HttpContext) HttpResponse(org.apache.http.HttpResponse) CloseableHttpResponse(org.apache.http.client.methods.CloseableHttpResponse) BasicHttpEntity(org.apache.http.entity.BasicHttpEntity) IOException(java.io.IOException) HandshakeException(org.apache.nifi.remote.exception.HandshakeException) HttpException(org.apache.http.HttpException) IOException(java.io.IOException) ExecutionException(java.util.concurrent.ExecutionException) URISyntaxException(java.net.URISyntaxException) TimeoutException(java.util.concurrent.TimeoutException) JsonParseException(com.fasterxml.jackson.core.JsonParseException) ProtocolException(org.apache.nifi.remote.exception.ProtocolException) JsonMappingException(com.fasterxml.jackson.databind.JsonMappingException) PortNotRunningException(org.apache.nifi.remote.exception.PortNotRunningException) MalformedURLException(java.net.MalformedURLException) UnknownPortException(org.apache.nifi.remote.exception.UnknownPortException) CertificateException(java.security.cert.CertificateException) SSLPeerUnverifiedException(javax.net.ssl.SSLPeerUnverifiedException) HttpAsyncRequestProducer(org.apache.http.nio.protocol.HttpAsyncRequestProducer) BasicAsyncResponseConsumer(org.apache.http.nio.protocol.BasicAsyncResponseConsumer) CloseableHttpAsyncClient(org.apache.http.impl.nio.client.CloseableHttpAsyncClient) ExecutionException(java.util.concurrent.ExecutionException) TimeoutException(java.util.concurrent.TimeoutException)

Example 2 with BasicAsyncResponseConsumer

use of org.apache.http.nio.protocol.BasicAsyncResponseConsumer in project nifi by apache.

the class SiteToSiteRestApiClient method openConnectionForSend.

public void openConnectionForSend(final String transactionUrl, final Peer peer) throws IOException {
    final CommunicationsSession commSession = peer.getCommunicationsSession();
    final String flowFilesPath = transactionUrl + "/flow-files";
    final HttpPost post = createPost(flowFilesPath);
    // Set uri so that it'll be used as transit uri.
    ((HttpCommunicationsSession) peer.getCommunicationsSession()).setDataTransferUrl(post.getURI().toString());
    post.setHeader("Content-Type", "application/octet-stream");
    post.setHeader("Accept", "text/plain");
    post.setHeader(HttpHeaders.PROTOCOL_VERSION, String.valueOf(transportProtocolVersionNegotiator.getVersion()));
    setHandshakeProperties(post);
    final CountDownLatch initConnectionLatch = new CountDownLatch(1);
    final URI requestUri = post.getURI();
    final PipedOutputStream outputStream = new PipedOutputStream();
    final PipedInputStream inputStream = new PipedInputStream(outputStream, DATA_PACKET_CHANNEL_READ_BUFFER_SIZE);
    final ReadableByteChannel dataPacketChannel = Channels.newChannel(inputStream);
    final HttpAsyncRequestProducer asyncRequestProducer = new HttpAsyncRequestProducer() {

        private final ByteBuffer buffer = ByteBuffer.allocate(DATA_PACKET_CHANNEL_READ_BUFFER_SIZE);

        private int totalRead = 0;

        private int totalProduced = 0;

        private boolean requestHasBeenReset = false;

        @Override
        public HttpHost getTarget() {
            return URIUtils.extractHost(requestUri);
        }

        @Override
        public HttpRequest generateRequest() throws IOException, HttpException {
            // Pass the output stream so that Site-to-Site client thread can send
            // data packet through this connection.
            logger.debug("sending data to {} has started...", flowFilesPath);
            ((HttpOutput) commSession.getOutput()).setOutputStream(outputStream);
            initConnectionLatch.countDown();
            final BasicHttpEntity entity = new BasicHttpEntity();
            entity.setChunked(true);
            entity.setContentType("application/octet-stream");
            post.setEntity(entity);
            return post;
        }

        private final AtomicBoolean bufferHasRemainingData = new AtomicBoolean(false);

        /**
         * If the proxy server requires authentication, the same POST request has to be sent again.
         * The first request will result 407, then the next one will be sent with auth headers and actual data.
         * This method produces a content only when it's need to be sent, to avoid producing the flow-file contents twice.
         * Whether we need to wait auth is determined heuristically by the previous POST request which creates transaction.
         * See {@link SiteToSiteRestApiClient#initiateTransactionForSend(HttpPost)} for further detail.
         */
        @Override
        public void produceContent(final ContentEncoder encoder, final IOControl ioControl) throws IOException {
            if (shouldCheckProxyAuth() && proxyAuthRequiresResend.get() && !requestHasBeenReset) {
                logger.debug("Need authentication with proxy server. Postpone producing content.");
                encoder.complete();
                return;
            }
            if (bufferHasRemainingData.get()) {
                // If there's remaining buffer last time, send it first.
                writeBuffer(encoder);
                if (bufferHasRemainingData.get()) {
                    return;
                }
            }
            int read;
            // or corresponding outputStream is closed.
            if ((read = dataPacketChannel.read(buffer)) > -1) {
                logger.trace("Read {} bytes from dataPacketChannel. {}", read, flowFilesPath);
                totalRead += read;
                buffer.flip();
                writeBuffer(encoder);
            } else {
                final long totalWritten = commSession.getOutput().getBytesWritten();
                logger.debug("sending data to {} has reached to its end. produced {} bytes by reading {} bytes from channel. {} bytes written in this transaction.", flowFilesPath, totalProduced, totalRead, totalWritten);
                if (totalRead != totalWritten || totalProduced != totalWritten) {
                    final String msg = "Sending data to %s has reached to its end, but produced : read : wrote byte sizes (%d : %d : %d) were not equal. Something went wrong.";
                    throw new RuntimeException(String.format(msg, flowFilesPath, totalProduced, totalRead, totalWritten));
                }
                transferDataLatch.countDown();
                encoder.complete();
                dataPacketChannel.close();
            }
        }

        private void writeBuffer(ContentEncoder encoder) throws IOException {
            while (buffer.hasRemaining()) {
                final int written = encoder.write(buffer);
                logger.trace("written {} bytes to encoder.", written);
                if (written == 0) {
                    logger.trace("Buffer still has remaining. {}", buffer);
                    bufferHasRemainingData.set(true);
                    return;
                }
                totalProduced += written;
            }
            bufferHasRemainingData.set(false);
            buffer.clear();
        }

        @Override
        public void requestCompleted(final HttpContext context) {
            logger.debug("Sending data to {} completed.", flowFilesPath);
            debugProxyAuthState(context);
        }

        @Override
        public void failed(final Exception ex) {
            final String msg = String.format("Failed to send data to %s due to %s", flowFilesPath, ex.toString());
            logger.error(msg, ex);
            eventReporter.reportEvent(Severity.WARNING, EVENT_CATEGORY, msg);
        }

        @Override
        public boolean isRepeatable() {
            // In order to pass authentication, request has to be repeatable.
            return true;
        }

        @Override
        public void resetRequest() throws IOException {
            logger.debug("Sending data request to {} has been reset...", flowFilesPath);
            requestHasBeenReset = true;
        }

        @Override
        public void close() throws IOException {
            logger.debug("Closing sending data request to {}", flowFilesPath);
            closeSilently(outputStream);
            closeSilently(dataPacketChannel);
            stopExtendingTtl();
        }
    };
    postResult = getHttpAsyncClient().execute(asyncRequestProducer, new BasicAsyncResponseConsumer(), null);
    try {
        // Need to wait the post request actually started so that we can write to its output stream.
        if (!initConnectionLatch.await(connectTimeoutMillis, TimeUnit.MILLISECONDS)) {
            throw new IOException("Awaiting initConnectionLatch has been timeout.");
        }
        // Started.
        transferDataLatch = new CountDownLatch(1);
        startExtendingTtl(transactionUrl, dataPacketChannel, null);
    } catch (final InterruptedException e) {
        throw new IOException("Awaiting initConnectionLatch has been interrupted.", e);
    }
}
Also used : HttpPost(org.apache.http.client.methods.HttpPost) ReadableByteChannel(java.nio.channels.ReadableByteChannel) HttpCommunicationsSession(org.apache.nifi.remote.io.http.HttpCommunicationsSession) ContentEncoder(org.apache.http.nio.ContentEncoder) IOControl(org.apache.http.nio.IOControl) HttpContext(org.apache.http.protocol.HttpContext) PipedOutputStream(java.io.PipedOutputStream) BasicHttpEntity(org.apache.http.entity.BasicHttpEntity) CommunicationsSession(org.apache.nifi.remote.protocol.CommunicationsSession) HttpCommunicationsSession(org.apache.nifi.remote.io.http.HttpCommunicationsSession) PipedInputStream(java.io.PipedInputStream) HttpOutput(org.apache.nifi.remote.io.http.HttpOutput) IOException(java.io.IOException) CountDownLatch(java.util.concurrent.CountDownLatch) URI(java.net.URI) ByteBuffer(java.nio.ByteBuffer) HandshakeException(org.apache.nifi.remote.exception.HandshakeException) HttpException(org.apache.http.HttpException) IOException(java.io.IOException) ExecutionException(java.util.concurrent.ExecutionException) URISyntaxException(java.net.URISyntaxException) TimeoutException(java.util.concurrent.TimeoutException) JsonParseException(com.fasterxml.jackson.core.JsonParseException) ProtocolException(org.apache.nifi.remote.exception.ProtocolException) JsonMappingException(com.fasterxml.jackson.databind.JsonMappingException) PortNotRunningException(org.apache.nifi.remote.exception.PortNotRunningException) MalformedURLException(java.net.MalformedURLException) UnknownPortException(org.apache.nifi.remote.exception.UnknownPortException) CertificateException(java.security.cert.CertificateException) SSLPeerUnverifiedException(javax.net.ssl.SSLPeerUnverifiedException) AtomicBoolean(java.util.concurrent.atomic.AtomicBoolean) HttpAsyncRequestProducer(org.apache.http.nio.protocol.HttpAsyncRequestProducer) BasicAsyncResponseConsumer(org.apache.http.nio.protocol.BasicAsyncResponseConsumer)

Aggregations

JsonParseException (com.fasterxml.jackson.core.JsonParseException)2 JsonMappingException (com.fasterxml.jackson.databind.JsonMappingException)2 IOException (java.io.IOException)2 MalformedURLException (java.net.MalformedURLException)2 URISyntaxException (java.net.URISyntaxException)2 CertificateException (java.security.cert.CertificateException)2 ExecutionException (java.util.concurrent.ExecutionException)2 TimeoutException (java.util.concurrent.TimeoutException)2 SSLPeerUnverifiedException (javax.net.ssl.SSLPeerUnverifiedException)2 HttpException (org.apache.http.HttpException)2 BasicHttpEntity (org.apache.http.entity.BasicHttpEntity)2 ContentEncoder (org.apache.http.nio.ContentEncoder)2 IOControl (org.apache.http.nio.IOControl)2 BasicAsyncResponseConsumer (org.apache.http.nio.protocol.BasicAsyncResponseConsumer)2 HttpAsyncRequestProducer (org.apache.http.nio.protocol.HttpAsyncRequestProducer)2 HttpContext (org.apache.http.protocol.HttpContext)2 HandshakeException (org.apache.nifi.remote.exception.HandshakeException)2 PortNotRunningException (org.apache.nifi.remote.exception.PortNotRunningException)2 ProtocolException (org.apache.nifi.remote.exception.ProtocolException)2 UnknownPortException (org.apache.nifi.remote.exception.UnknownPortException)2