com.joyent.manta.http.MantaConnectionFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.joyent.manta.http.MantaConnectionFactory.java

Source

/*
 * Copyright (c) 2016-2017, Joyent, Inc. All rights reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package com.joyent.manta.http;

import com.joyent.http.signature.ThreadLocalSigner;
import com.joyent.manta.client.MantaMBeanable;
import com.joyent.manta.config.ConfigContext;
import com.joyent.manta.config.DefaultsConfigContext;
import com.joyent.manta.config.MantaClientMetricConfiguration;
import com.joyent.manta.exception.ConfigurationException;
import com.joyent.manta.util.MantaVersion;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
import org.apache.http.HttpHost;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.ServiceUnavailableRetryStrategy;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.DnsResolver;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.HttpConnectionFactory;
import org.apache.http.conn.ManagedHttpClientConnection;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultBackoffStrategy;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.io.DefaultHttpRequestWriterFactory;
import org.apache.http.impl.io.DefaultHttpResponseParserFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.management.DynamicMBean;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.security.KeyPair;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Factory class that creates instances of
 * {@link org.apache.http.client.HttpClient} configured for use with
 * HTTP signature based authentication.
 * <p>
 * Note: This class used to contain convenience methods for building
 * {@link org.apache.http.client.methods.HttpUriRequest} objects, those have been moved
 * to {@link MantaHttpRequestFactory}.
 *
 * @author <a href="https://github.com/dekobon">Elijah Zupancic</a>
 * @since 3.0.0
 */
public class MantaConnectionFactory implements Closeable, MantaMBeanable, RetryConfigAware {
    /**
     * Logger instance.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(MantaConnectionFactory.class);

    /**
     * Default DNS resolver for all connections to the Manta.
     */
    private static final DnsResolver DNS_RESOLVER = new ShufflingDnsResolver();

    /**
     * User Agent string identifying Manta Client and Java version.
     */
    private static final String USER_AGENT = String.format("Java-Manta-SDK/%s (Java/%s/%s)", MantaVersion.VERSION,
            System.getProperty("java.version"), System.getProperty("java.vendor"));

    /**
     * Configuration context that provides connection details.
     */
    private final ConfigContext config;

    /**
     * Apache HTTP Client connection builder helper.
     */
    private final HttpClientBuilder httpClientBuilder;

    /**
     * Connection manager instance that is associated with a single Manta client.
     */
    private final HttpClientConnectionManager connectionManager;

    /**
     * The retry handler used to handle transport errors during requests if automatic retries are enabled, or null.
     */
    private final HttpRequestRetryHandler retryHandler;

    /**
     * The retry handler used to handle 5xx errors during requests if automatic retries are enabled, or null.
     */
    private final ServiceUnavailableRetryStrategy serviceUnavailableRetryStrategy;

    /**
     * Create new instance using the passed configuration.
     *
     * @param config configuration of the connection parameters
     * @param keyPair cryptographic signing key pair used for HTTP signatures
     * @param signer Signer configured to use the given keyPair
     */
    @Deprecated
    public MantaConnectionFactory(final ConfigContext config, final KeyPair keyPair,
            final ThreadLocalSigner signer) {
        this(config, null);
    }

    /**
     * Create a new instance based on a shared {@link HttpClientBuilder} and {@link HttpClientConnectionManager}.
     *
     * @param config configuration of the connection parameters
     * @param keyPair cryptographic signing key pair used for HTTP signatures
     * @param signer Signer configured to use the given keyPair
     * @param connectionFactoryConfigurator existing HttpClient objects to reuse
     */
    @Deprecated
    public MantaConnectionFactory(final ConfigContext config, final KeyPair keyPair, final ThreadLocalSigner signer,
            final MantaConnectionFactoryConfigurator connectionFactoryConfigurator) {
        this(config, connectionFactoryConfigurator);
    }

    /**
     * Create new instance using the passed configuration.
     *
     * @param config configuration of the connection parameters
     */
    public MantaConnectionFactory(final ConfigContext config) {
        this(config, null);
    }

    /**
     * Create new instance using the passed configuration.
     *
     * @param config configuration of the connection parameters
     * @param connectionFactoryConfigurator existing HttpClient objects to reuse
     */
    public MantaConnectionFactory(final ConfigContext config,
            final MantaConnectionFactoryConfigurator connectionFactoryConfigurator) {
        this(config, connectionFactoryConfigurator, null);
    }

    /**
     * Create new instance using the passed configuration.
     *
     * @param config configuration of the connection parameters
     * @param connectionFactoryConfigurator existing HttpClient objects to reuse
     * @param metricConfig potentially-null configuration for tracking client metrics
     */
    public MantaConnectionFactory(final ConfigContext config,
            final MantaConnectionFactoryConfigurator connectionFactoryConfigurator,
            final MantaClientMetricConfiguration metricConfig) {
        this.config = Validate.notNull(config, "Configuration context must not be null");

        if (connectionFactoryConfigurator != null) {
            this.connectionManager = null;
            this.httpClientBuilder = connectionFactoryConfigurator.getHttpClientBuilder();
        } else {
            this.connectionManager = buildConnectionManager(metricConfig);
            this.httpClientBuilder = createStandardBuilder(metricConfig);
        }

        // Manta does not generally return redirects, but let's be safe and disable them in the client
        this.httpClientBuilder.disableRedirectHandling();

        if (config.getRetries() > 0) {
            this.retryHandler = new MantaHttpRequestRetryHandler(config.getRetries(), metricConfig);
            this.serviceUnavailableRetryStrategy = new MantaServiceUnavailableRetryStrategy(config.getRetries());

            this.httpClientBuilder.setRetryHandler(this.retryHandler);
            this.httpClientBuilder.setServiceUnavailableRetryStrategy(this.serviceUnavailableRetryStrategy);
        } else {
            LOGGER.info("Retry of failed requests is disabled");
            this.retryHandler = null;
            this.serviceUnavailableRetryStrategy = null;

            this.httpClientBuilder.disableAutomaticRetries();
        }

        // attach the connection manager if it was created by us
        // users providing a custom HttpClientBuilder are expected to wire this up themselves
        if (this.connectionManager != null) {
            this.httpClientBuilder.setConnectionManager(this.connectionManager);
        }
    }

    /**
     * Builds and configures a default connection factory instance.
     *
     * @return configured connection factory
     */
    protected HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> buildHttpConnectionFactory() {
        return new ManagedHttpClientConnectionFactory(new DefaultHttpRequestWriterFactory(),
                new DefaultHttpResponseParserFactory());
    }

    /**
     * Builds a socket configuration customized for Manta.
     *
     * @return fully configured instance
     */
    protected SocketConfig buildSocketConfig() {
        final int socketTimeout = ObjectUtils.firstNonNull(config.getTcpSocketTimeout(),
                DefaultsConfigContext.DEFAULT_TCP_SOCKET_TIMEOUT);

        return SocketConfig.custom()
                /* Disable Nagle's algorithm for this connection.  Written data
                 * to the network is not buffered pending acknowledgement of
                 * previously written data.
                 */
                .setTcpNoDelay(true)
                /* Set a timeout on blocking Socket operations. */
                .setSoTimeout(socketTimeout).setSoKeepAlive(true).build();
    }

    /**
     * Builds and configures a {@link ConnectionConfig} instance.
     *
     * @return fully configured instance
     */
    protected ConnectionConfig buildConnectionConfig() {
        final int bufferSize = ObjectUtils.firstNonNull(config.getHttpBufferSize(),
                DefaultsConfigContext.DEFAULT_HTTP_BUFFER_SIZE);

        return ConnectionConfig.custom().setBufferSize(bufferSize).build();
    }

    /**
     * Configures a connection manager with all of the setting needed to connect
     * to Manta.
     *
     * @param metricConfig potentially-null configuration for tracking client metrics
     * @return fully configured connection manager
     */
    protected HttpClientConnectionManager buildConnectionManager(
            final MantaClientMetricConfiguration metricConfig) {
        final int maxConns = ObjectUtils.firstNonNull(config.getMaximumConnections(),
                DefaultsConfigContext.DEFAULT_MAX_CONNS);

        final ConnectionSocketFactory sslConnectionSocketFactory = new MantaSSLConnectionSocketFactory(this.config);

        final RegistryBuilder<ConnectionSocketFactory> registryBuilder = RegistryBuilder.create();

        final Registry<ConnectionSocketFactory> socketFactoryRegistry = registryBuilder
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", sslConnectionSocketFactory).build();

        final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory = buildHttpConnectionFactory();

        final PoolingHttpClientConnectionManager connManager;
        if (metricConfig != null) {
            connManager = new InstrumentedPoolingHttpClientConnectionManager(metricConfig.getRegistry(),
                    socketFactoryRegistry, connFactory, null, DNS_RESOLVER, -1, TimeUnit.MILLISECONDS);
        } else {
            connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry, connFactory, DNS_RESOLVER);
        }

        connManager.setDefaultMaxPerRoute(maxConns);
        connManager.setMaxTotal(maxConns);
        connManager.setDefaultSocketConfig(buildSocketConfig());
        connManager.setDefaultConnectionConfig(buildConnectionConfig());

        return connManager;
    }

    /**
     * Configures the builder class with all of the settings needed to connect to
     * Manta.
     *
     * @param metricConfig nullable configuration for client metrics tracking
     * @return configured instance
     */
    protected HttpClientBuilder createStandardBuilder(final MantaClientMetricConfiguration metricConfig) {
        final int maxConns = ObjectUtils.firstNonNull(config.getMaximumConnections(),
                DefaultsConfigContext.DEFAULT_MAX_CONNS);

        final int timeout = ObjectUtils.firstNonNull(config.getTimeout(),
                DefaultsConfigContext.DEFAULT_HTTP_TIMEOUT);

        final int connectionRequestTimeout = ObjectUtils.firstNonNull(config.getConnectionRequestTimeout(),
                DefaultsConfigContext.DEFAULT_CONNECTION_REQUEST_TIMEOUT);

        final Integer expectContinueTimeout = config.getExpectContinueTimeout();
        final boolean expectContinueEnabled = expectContinueTimeout != null;

        final RequestConfig requestConfig = RequestConfig.custom().setAuthenticationEnabled(false)
                .setSocketTimeout(timeout).setConnectionRequestTimeout(connectionRequestTimeout)
                .setContentCompressionEnabled(true).setExpectContinueEnabled(expectContinueEnabled).build();

        final MantaHttpRequestExecutor requestExecutor = MantaHttpRequestExecutor.Builder.create()
                .setMetricConfiguration(metricConfig).setWaitForContinue(expectContinueTimeout).build();

        final HttpClientBuilder builder = HttpClients.custom().disableAuthCaching().disableCookieManagement()
                .setUserAgent(USER_AGENT).setConnectionReuseStrategy(new DefaultConnectionReuseStrategy())
                .setMaxConnTotal(maxConns).setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
                .setDefaultRequestConfig(requestConfig).setConnectionManagerShared(false)
                .setRequestExecutor(requestExecutor).setConnectionBackoffStrategy(new DefaultBackoffStrategy());

        final HttpHost proxyHost = findProxyServer();

        if (proxyHost != null) {
            builder.setProxy(proxyHost);
        }

        return builder;
    }

    /**
     * Finds the host of the proxy server that was configured as part of the
     * JVM settings.
     *
     * @return proxy server as {@link HttpHost}, if no proxy then null
     */
    protected HttpHost findProxyServer() {
        final ProxySelector proxySelector = ProxySelector.getDefault();
        List<Proxy> proxies = proxySelector.select(URI.create(config.getMantaURL()));

        if (!proxies.isEmpty()) {
            /* The Apache HTTP Client doesn't understand the concept of multiple
             * proxies, so we use only the first one returned. */
            final Proxy proxy = proxies.get(0);

            switch (proxy.type()) {
            case DIRECT:
                return null;
            case SOCKS:
                throw new ConfigurationException("SOCKS proxies are unsupported");
            default:
                // do nothing and fall through
            }

            if (proxy.address() instanceof InetSocketAddress) {
                InetSocketAddress sa = (InetSocketAddress) proxy.address();

                return new HttpHost(sa.getHostName(), sa.getPort());
            } else {
                String msg = String.format(
                        "Expecting proxy to be instance of InetSocketAddress. " + " Actually: %s", proxy.address());
                throw new ConfigurationException(msg);
            }
        } else {
            return null;
        }
    }

    /**
     * Creates a new configured instance of {@link CloseableHttpClient} based
     * on the factory's configuration.
     *
     * @return new connection object instance
     */
    public CloseableHttpClient createConnection() {
        return httpClientBuilder.build();
    }

    @Override
    public DynamicMBean toMBean() {
        if (!(connectionManager instanceof PoolingHttpClientConnectionManager)) {
            return null;
        }

        return new PoolStatsMBean((PoolingHttpClientConnectionManager) connectionManager);
    }

    @Override
    public boolean isRetryEnabled() {
        return ObjectUtils.allNotNull(this.retryHandler, this.serviceUnavailableRetryStrategy);
    }

    @Override
    public boolean isRetryCancellable() {
        return this.retryHandler instanceof HttpContextRetryCancellation
                && this.serviceUnavailableRetryStrategy instanceof HttpContextRetryCancellation;
    }

    @Override
    public void close() throws IOException {
        if (connectionManager == null) {
            // user provided their own connectionManager in the httpClientBuilder
            return;
        }

        connectionManager.shutdown();
    }
}