org.apache.nifi.cluster.manager.impl.HttpRequestReplicatorImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.nifi.cluster.manager.impl.HttpRequestReplicatorImpl.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.nifi.cluster.manager.impl;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.filter.GZIPContentEncodingFilter;
import com.sun.jersey.core.util.MultivaluedMapImpl;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;

import org.apache.nifi.cluster.manager.HttpRequestReplicator;
import org.apache.nifi.cluster.manager.NodeResponse;
import org.apache.nifi.cluster.manager.exception.UriConstructionException;
import org.apache.nifi.cluster.protocol.NodeIdentifier;
import org.apache.nifi.logging.NiFiLog;
import org.apache.nifi.util.FormatUtils;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An implementation of the <code>HttpRequestReplicator</code> interface. This implementation parallelizes the node HTTP requests using the given <code>ExecutorService</code> instance. Individual
 * requests may have connection and read timeouts set, which may be set during instance construction. Otherwise, the default is not to timeout.
 *
 * If a node protocol scheme is provided during construction, then all requests will be replicated using the given scheme. If null is provided as the scheme (the default), then the requests will be
 * replicated using the scheme of the original URI.
 *
 * Clients must call start() and stop() to initialize and shutdown the instance. The instance must be started before issuing any replication requests.
 *
 */
public class HttpRequestReplicatorImpl implements HttpRequestReplicator {

    // defaults
    private static final int DEFAULT_SHUTDOWN_REPLICATOR_SECONDS = 30;

    // logger
    private static final Logger logger = new NiFiLog(LoggerFactory.getLogger(HttpRequestReplicatorImpl.class));

    // final members
    private final Client client; // the client to use for issuing requests
    private final int numThreads; // number of threads to use for request replication
    private final int connectionTimeoutMs; // connection timeout per node request
    private final int readTimeoutMs; // read timeout per node request

    // members
    private ExecutorService executorService;
    private int shutdownReplicatorSeconds = DEFAULT_SHUTDOWN_REPLICATOR_SECONDS;

    // guarded by synchronized method access in support of multithreaded replication
    private String nodeProtocolScheme = null;

    /**
     * Creates an instance. The connection timeout and read timeout will be infinite.
     *
     * @param numThreads the number of threads to use when parallelizing requests
     * @param client a client for making requests
     */
    public HttpRequestReplicatorImpl(final int numThreads, final Client client) {
        this(numThreads, client, "0 sec", "0 sec");
    }

    /**
     * Creates an instance.
     *
     * @param numThreads the number of threads to use when parallelizing requests
     * @param client a client for making requests
     * @param connectionTimeout the connection timeout specified in milliseconds
     * @param readTimeout the read timeout specified in milliseconds
     */
    public HttpRequestReplicatorImpl(final int numThreads, final Client client, final String connectionTimeout,
            final String readTimeout) {

        if (numThreads <= 0) {
            throw new IllegalArgumentException("The number of threads must be greater than zero.");
        } else if (client == null) {
            throw new IllegalArgumentException("Client may not be null.");
        }

        this.numThreads = numThreads;
        this.client = client;
        this.connectionTimeoutMs = (int) FormatUtils.getTimeDuration(connectionTimeout, TimeUnit.MILLISECONDS);
        this.readTimeoutMs = (int) FormatUtils.getTimeDuration(readTimeout, TimeUnit.MILLISECONDS);

        client.getProperties().put(ClientConfig.PROPERTY_CONNECT_TIMEOUT, connectionTimeoutMs);
        client.getProperties().put(ClientConfig.PROPERTY_READ_TIMEOUT, readTimeoutMs);
        client.getProperties().put(ClientConfig.PROPERTY_FOLLOW_REDIRECTS, Boolean.TRUE);
    }

    @Override
    public void start() {
        if (isRunning()) {
            throw new IllegalStateException("Instance is already started.");
        }
        executorService = Executors.newFixedThreadPool(numThreads);
    }

    @Override
    public boolean isRunning() {
        return executorService != null && !executorService.isShutdown();
    }

    @Override
    public void stop() {

        if (!isRunning()) {
            throw new IllegalStateException("Instance is already stopped.");
        }

        // shutdown executor service
        try {
            if (getShutdownReplicatorSeconds() <= 0) {
                executorService.shutdownNow();
            } else {
                executorService.shutdown();
            }
            executorService.awaitTermination(getShutdownReplicatorSeconds(), TimeUnit.SECONDS);
        } catch (final InterruptedException ex) {
            Thread.currentThread().interrupt();
        } finally {
            if (executorService.isTerminated()) {
                logger.info("HTTP Request Replicator has been terminated successfully.");
            } else {
                logger.warn(
                        "HTTP Request Replicator has not terminated properly.  There exists an uninterruptable thread that will take an indeterminate amount of time to stop.");
            }
        }
    }

    /**
     * Sets the protocol scheme to use when issuing requests to nodes.
     *
     * @param nodeProtocolScheme the scheme. Valid values are "http", "https", or null. If null is specified, then the scheme of the originating request is used when replicating that request.
     */
    public synchronized void setNodeProtocolScheme(final String nodeProtocolScheme) {
        if (StringUtils.isNotBlank(nodeProtocolScheme)) {
            if (!"http".equalsIgnoreCase(nodeProtocolScheme) && !"https".equalsIgnoreCase(nodeProtocolScheme)) {
                throw new IllegalArgumentException("Node Protocol Scheme must be either HTTP or HTTPS");
            }
        }
        this.nodeProtocolScheme = nodeProtocolScheme;
    }

    public synchronized String getNodeProtocolScheme() {
        return nodeProtocolScheme;
    }

    private synchronized String getNodeProtocolScheme(final URI uri) {
        // if we are not configured to use a protocol scheme, then use the uri's scheme
        if (StringUtils.isBlank(nodeProtocolScheme)) {
            return uri.getScheme();
        }
        return nodeProtocolScheme;
    }

    public int getConnectionTimeoutMs() {
        return connectionTimeoutMs;
    }

    public int getReadTimeoutMs() {
        return readTimeoutMs;
    }

    public int getShutdownReplicatorSeconds() {
        return shutdownReplicatorSeconds;
    }

    public void setShutdownReplicatorSeconds(int shutdownReplicatorSeconds) {
        this.shutdownReplicatorSeconds = shutdownReplicatorSeconds;
    }

    @Override
    public Set<NodeResponse> replicate(final Set<NodeIdentifier> nodeIds, final String method, final URI uri,
            final Map<String, List<String>> parameters, final Map<String, String> headers)
            throws UriConstructionException {
        if (nodeIds == null) {
            throw new IllegalArgumentException("Node IDs may not be null.");
        } else if (method == null) {
            throw new IllegalArgumentException("HTTP method may not be null.");
        } else if (uri == null) {
            throw new IllegalArgumentException("URI may not be null.");
        } else if (parameters == null) {
            throw new IllegalArgumentException("Parameters may not be null.");
        } else if (headers == null) {
            throw new IllegalArgumentException("HTTP headers map may not be null.");
        }
        return replicateHelper(nodeIds, method, getNodeProtocolScheme(uri), uri.getPath(), parameters,
                /* entity */ null, headers);
    }

    @Override
    public Set<NodeResponse> replicate(final Set<NodeIdentifier> nodeIds, final String method, final URI uri,
            final Object entity, final Map<String, String> headers) throws UriConstructionException {
        if (nodeIds == null) {
            throw new IllegalArgumentException("Node IDs may not be null.");
        } else if (method == null) {
            throw new IllegalArgumentException("HTTP method may not be null.");
        } else if (method.equalsIgnoreCase(HttpMethod.DELETE) || method.equalsIgnoreCase(HttpMethod.GET)
                || method.equalsIgnoreCase(HttpMethod.HEAD) || method.equalsIgnoreCase(HttpMethod.OPTIONS)) {
            throw new IllegalArgumentException(
                    "HTTP (DELETE | GET | HEAD | OPTIONS) requests cannot have a body containing an entity.");
        } else if (uri == null) {
            throw new IllegalArgumentException("URI may not be null.");
        } else if (entity == null) {
            throw new IllegalArgumentException("Entity may not be null.");
        } else if (headers == null) {
            throw new IllegalArgumentException("HTTP headers map may not be null.");
        }
        return replicateHelper(nodeIds, method, getNodeProtocolScheme(uri), uri.getPath(), /* parameters */ null,
                entity, headers);
    }

    private Set<NodeResponse> replicateHelper(final Set<NodeIdentifier> nodeIds, final String method,
            final String scheme, final String path, final Map<String, List<String>> parameters, final Object entity,
            final Map<String, String> headers) throws UriConstructionException {

        if (nodeIds.isEmpty()) {
            return new HashSet<>(); // return quickly for trivial case
        }

        final CompletionService<NodeResponse> completionService = new ExecutorCompletionService<>(executorService);

        // keeps track of future requests so that failed requests can be tied back to the failing node
        final Collection<NodeHttpRequestFutureWrapper> futureNodeHttpRequests = new ArrayList<>();

        // construct the URIs for the nodes
        final Map<NodeIdentifier, URI> uriMap = new HashMap<>();
        try {
            for (final NodeIdentifier nodeId : nodeIds) {
                final URI nodeUri = new URI(scheme, null, nodeId.getApiAddress(), nodeId.getApiPort(), path,
                        /* query */ null, /* fragment */ null);
                uriMap.put(nodeId, nodeUri);
            }
        } catch (final URISyntaxException use) {
            throw new UriConstructionException(use);
        }

        // submit the requests to the nodes
        final String requestId = UUID.randomUUID().toString();
        headers.put(WebClusterManager.REQUEST_ID_HEADER, requestId);
        for (final Map.Entry<NodeIdentifier, URI> entry : uriMap.entrySet()) {
            final NodeIdentifier nodeId = entry.getKey();
            final URI nodeUri = entry.getValue();
            final NodeHttpRequestCallable callable = (entity == null)
                    ? new NodeHttpRequestCallable(nodeId, method, nodeUri, parameters, headers)
                    : new NodeHttpRequestCallable(nodeId, method, nodeUri, entity, headers);
            futureNodeHttpRequests.add(
                    new NodeHttpRequestFutureWrapper(nodeId, method, nodeUri, completionService.submit(callable)));
        }

        // get the node responses
        final Set<NodeResponse> result = new HashSet<>();
        for (int i = 0; i < nodeIds.size(); i++) {

            // keeps track of the original request information in case we receive an exception
            NodeHttpRequestFutureWrapper futureNodeHttpRequest = null;
            try {

                // get the future resource response for the node
                final Future<NodeResponse> futureNodeResourceResponse = completionService.take();

                // find the original request by comparing the submitted future with the future returned by the completion service
                for (final NodeHttpRequestFutureWrapper futureNodeHttpRequestElem : futureNodeHttpRequests) {
                    if (futureNodeHttpRequestElem.getFuture() == futureNodeResourceResponse) {
                        futureNodeHttpRequest = futureNodeHttpRequestElem;
                    }
                }

                // try to retrieve the node response and add to result
                final NodeResponse nodeResponse = futureNodeResourceResponse.get();
                result.add(nodeResponse);

            } catch (final InterruptedException | ExecutionException ex) {

                logger.warn(
                        "Node request for " + futureNodeHttpRequest.getNodeId() + " encountered exception: " + ex,
                        ex);

                // create node response with the thrown exception and add to result
                final NodeResponse nodeResponse = new NodeResponse(futureNodeHttpRequest.getNodeId(),
                        futureNodeHttpRequest.getHttpMethod(), futureNodeHttpRequest.getRequestUri(), ex);
                result.add(nodeResponse);

            }
        }

        if (logger.isDebugEnabled()) {
            NodeResponse min = null;
            NodeResponse max = null;
            long nanosSum = 0L;
            int nanosAdded = 0;

            for (final NodeResponse response : result) {
                final long requestNanos = response.getRequestDuration(TimeUnit.NANOSECONDS);
                final long minNanos = (min == null) ? -1 : min.getRequestDuration(TimeUnit.NANOSECONDS);
                final long maxNanos = (max == null) ? -1 : max.getRequestDuration(TimeUnit.NANOSECONDS);

                if (requestNanos < minNanos || minNanos < 0L) {
                    min = response;
                }

                if (requestNanos > maxNanos || maxNanos < 0L) {
                    max = response;
                }

                if (requestNanos >= 0L) {
                    nanosSum += requestNanos;
                    nanosAdded++;
                }
            }

            final StringBuilder sb = new StringBuilder();
            sb.append("Node Responses for ").append(method).append(" ").append(path).append(" (Request ID ")
                    .append(requestId).append("):\n");
            for (final NodeResponse response : result) {
                sb.append(response).append("\n");
            }

            final long averageNanos = (nanosAdded == 0) ? -1L : nanosSum / nanosAdded;
            final long averageMillis = (averageNanos < 0) ? averageNanos
                    : TimeUnit.MILLISECONDS.convert(averageNanos, TimeUnit.NANOSECONDS);
            logger.debug("For {} {} (Request ID {}), minimum response time = {}, max = {}, average = {} ms", method,
                    path, requestId, min, max, averageMillis);
            logger.debug(sb.toString());
        }

        return result;
    }

    /**
     * Wraps a future node response with info from originating request. This coupling allows for futures that encountered exceptions to be linked back to the failing node and better reported.
     */
    private class NodeHttpRequestFutureWrapper {

        private final NodeIdentifier nodeId;

        private final String httpMethod;

        private final URI requestUri;

        private final Future<NodeResponse> future;

        public NodeHttpRequestFutureWrapper(final NodeIdentifier nodeId, final String httpMethod,
                final URI requestUri, final Future<NodeResponse> future) {
            if (nodeId == null) {
                throw new IllegalArgumentException("Node ID may not be null.");
            } else if (StringUtils.isBlank(httpMethod)) {
                throw new IllegalArgumentException("Http method may not be null or empty.");
            } else if (requestUri == null) {
                throw new IllegalArgumentException("Request URI may not be null.");
            } else if (future == null) {
                throw new IllegalArgumentException("Future may not be null.");
            }
            this.nodeId = nodeId;
            this.httpMethod = httpMethod;
            this.requestUri = requestUri;
            this.future = future;
        }

        public NodeIdentifier getNodeId() {
            return nodeId;
        }

        public String getHttpMethod() {
            return httpMethod;
        }

        public URI getRequestUri() {
            return requestUri;
        }

        public Future<NodeResponse> getFuture() {
            return future;
        }
    }

    /**
     * A Callable for making an HTTP request to a single node and returning its response.
     */
    private class NodeHttpRequestCallable implements Callable<NodeResponse> {

        private final NodeIdentifier nodeId;
        private final String method;
        private final URI uri;
        private final Object entity;
        private final Map<String, List<String>> parameters = new HashMap<>();
        private final Map<String, String> headers = new HashMap<>();

        private NodeHttpRequestCallable(final NodeIdentifier nodeId, final String method, final URI uri,
                final Object entity, final Map<String, String> headers) {
            this.nodeId = nodeId;
            this.method = method;
            this.uri = uri;
            this.entity = entity;
            this.headers.putAll(headers);
        }

        private NodeHttpRequestCallable(final NodeIdentifier nodeId, final String method, final URI uri,
                final Map<String, List<String>> parameters, final Map<String, String> headers) {
            this.nodeId = nodeId;
            this.method = method;
            this.uri = uri;
            this.entity = null;
            this.parameters.putAll(parameters);
            this.headers.putAll(headers);
        }

        @Override
        public NodeResponse call() {

            try {
                // create and send the request
                final WebResource.Builder resourceBuilder = getResourceBuilder();
                final String requestId = headers.get("x-nifi-request-id");

                final long startNanos = System.nanoTime();
                final ClientResponse clientResponse;
                if (HttpMethod.DELETE.equalsIgnoreCase(method)) {
                    clientResponse = resourceBuilder.delete(ClientResponse.class);
                } else if (HttpMethod.GET.equalsIgnoreCase(method)) {
                    clientResponse = resourceBuilder.get(ClientResponse.class);
                } else if (HttpMethod.HEAD.equalsIgnoreCase(method)) {
                    clientResponse = resourceBuilder.head();
                } else if (HttpMethod.OPTIONS.equalsIgnoreCase(method)) {
                    clientResponse = resourceBuilder.options(ClientResponse.class);
                } else if (HttpMethod.POST.equalsIgnoreCase(method)) {
                    clientResponse = resourceBuilder.post(ClientResponse.class);
                } else if (HttpMethod.PUT.equalsIgnoreCase(method)) {
                    clientResponse = resourceBuilder.put(ClientResponse.class);
                } else {
                    throw new IllegalArgumentException(
                            "HTTP Method '" + method + "' not supported for request replication.");
                }

                // create and return the response
                return new NodeResponse(nodeId, method, uri, clientResponse, System.nanoTime() - startNanos,
                        requestId);

            } catch (final UniformInterfaceException | IllegalArgumentException t) {
                return new NodeResponse(nodeId, method, uri, t);
            }

        }

        private WebResource.Builder getResourceBuilder() {

            // convert parameters to a more convenient data structure
            final MultivaluedMap<String, String> map = new MultivaluedMapImpl();
            map.putAll(parameters);

            // create the resource
            WebResource resource = client.resource(uri);

            if (WebClusterManager.isResponseInterpreted(uri, method)) {
                resource.addFilter(new GZIPContentEncodingFilter(false));
            }

            // set the parameters as either query parameters or as request body
            final WebResource.Builder builder;
            if (HttpMethod.DELETE.equalsIgnoreCase(method) || HttpMethod.HEAD.equalsIgnoreCase(method)
                    || HttpMethod.GET.equalsIgnoreCase(method) || HttpMethod.OPTIONS.equalsIgnoreCase(method)) {
                resource = resource.queryParams(map);
                builder = resource.getRequestBuilder();
            } else {
                if (entity == null) {
                    builder = resource.entity(map);
                } else {
                    builder = resource.entity(entity);
                }
            }

            // set headers
            boolean foundContentType = false;
            for (final Map.Entry<String, String> entry : headers.entrySet()) {
                builder.header(entry.getKey(), entry.getValue());
                if (entry.getKey().equalsIgnoreCase("content-type")) {
                    foundContentType = true;
                }
            }

            // set default content type
            if (!foundContentType) {
                // set default content type
                builder.type(MediaType.APPLICATION_FORM_URLENCODED);
            }

            return builder;
        }

    }
}