com.ctriposs.r2.transport.http.client.HttpClientFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.ctriposs.r2.transport.http.client.HttpClientFactory.java

Source

/**
 * Copyright (C) 2014 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * Licensed 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 com.ctriposs.r2.transport.http.client;

import com.ctriposs.r2.util.ConfigValueExtractor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;

import org.apache.commons.lang.StringUtils;
import org.jboss.netty.channel.socket.ClientSocketChannelFactory;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ctriposs.common.callback.Callback;
import com.ctriposs.common.util.None;
import com.ctriposs.r2.filter.FilterChain;
import com.ctriposs.r2.filter.FilterChains;
import com.ctriposs.r2.filter.compression.ClientCompressionFilter;
import com.ctriposs.r2.filter.compression.EncodingType;
import com.ctriposs.r2.filter.transport.FilterChainClient;
import com.ctriposs.r2.message.RequestContext;
import com.ctriposs.r2.message.rest.RestRequest;
import com.ctriposs.r2.message.rest.RestResponse;
import com.ctriposs.r2.transport.common.TransportClientFactory;
import com.ctriposs.r2.transport.common.bridge.client.TransportClient;
import com.ctriposs.r2.transport.common.bridge.common.TransportCallback;
import com.ctriposs.r2.util.NamedThreadFactory;

/**
 * A factory for HttpNettyClient instances.
 *
 * All clients created by the factory will share the same resources, in particular the
 * {@link ClientSocketChannelFactory} and {@link ScheduledExecutorService}.
 *
 * In order to shutdown cleanly, all clients issued by the factory should be shutdown via
 * {@link TransportClient#shutdown(com.ctriposs.common.callback.Callback)} and the factory
 * itself should be shut down via one of the following two methods:
 * <ul>
 * <li>{@link #shutdown(com.ctriposs.common.callback.Callback)}</li>
 * <li>
 * {@link #shutdown(com.ctriposs.common.callback.Callback, long, java.util.concurrent.TimeUnit)}
 * </li>
 * </ul>
 *
 * See the method descriptions for more details. Note that factory shutdown and shutdown
 * of the clients can be initiated in any order.
 *
 */
public class HttpClientFactory implements TransportClientFactory {
    private static final Logger LOG = LoggerFactory.getLogger(HttpClientFactory.class);

    public static final String HTTP_QUERY_POST_THRESHOLD = "http.queryPostThreshold";
    public static final String HTTP_REQUEST_TIMEOUT = "http.requestTimeout";
    public static final String HTTP_MAX_RESPONSE_SIZE = "http.maxResponseSize";
    public static final String HTTP_POOL_SIZE = "http.poolSize";
    public static final String HTTP_POOL_WAITER_SIZE = "http.poolWaiterSize";
    public static final String HTTP_IDLE_TIMEOUT = "http.idleTimeout";
    public static final String HTTP_SHUTDOWN_TIMEOUT = "http.shutdownTimeout";
    public static final String HTTP_SSL_CONTEXT = "http.sslContext";
    public static final String HTTP_SSL_PARAMS = "http.sslParams";
    public static final String HTTP_RESPONSE_COMPRESSION_OPERATIONS = "http.responseCompressionOperations";
    public static final String HTTP_SERVICE_NAME = "http.serviceName";
    public static final String HTTP_POOL_STRATEGY = "http.poolStrategy";
    public static final String HTTP_POOL_MIN_SIZE = "http.poolMinSize";

    public static final int DEFAULT_POOL_WAITER_SIZE = Integer.MAX_VALUE;
    public static final int DEFAULT_POOL_SIZE = 200;
    public static final int DEFAULT_REQUEST_TIMEOUT = 10000;
    public static final int DEFAULT_IDLE_TIMEOUT = 30000;
    public static final int DEFAULT_SHUTDOWN_TIMEOUT = 5000;
    public static final int DEFAULT_MAX_RESPONSE_SIZE = 1024 * 1024 * 2;
    public static final String DEFAULT_CLIENT_NAME = "noNameSpecifiedClient";
    public static final AsyncPoolImpl.Strategy DEFAULT_POOL_STRATEGY = AsyncPoolImpl.Strategy.MRU;
    public static final int DEFAULT_POOL_MIN_SIZE = 0;
    public static final AbstractJmxManager NULL_JMX_MANAGER = new AbstractJmxManager() {
        @Override
        public void onProviderCreate(PoolStatsProvider provider) {
        }

        @Override
        public void onProviderShutdown(PoolStatsProvider provider) {
        }
    };

    private static final String LIST_SEPARATOR = ",";

    private final ClientSocketChannelFactory _channelFactory;
    private final ScheduledExecutorService _executor;
    private final ExecutorService _callbackExecutor;
    private final boolean _shutdownFactory;
    private final boolean _shutdownExecutor;
    private final boolean _shutdownCallbackExecutor;
    private final FilterChain _filters;

    private final AtomicBoolean _finishingShutdown = new AtomicBoolean(false);
    private volatile ScheduledFuture<?> _shutdownTimeoutTask;
    private final AbstractJmxManager _jmxManager;

    // All fields below protected by _mutex
    private final Object _mutex = new Object();
    private boolean _running = true;
    private int _clientsOutstanding = 0;
    private Callback<None> _factoryShutdownCallback;

    /**
     * Construct a new instance using an empty filter chain.
     */
    public HttpClientFactory() {
        this(FilterChains.empty());
    }

    /**
     * Construct a new instance with a specified callback executor.
     *
     * @param callbackExecutor an optional executor to invoke user callbacks that otherwise
     *          will be invoked by scheduler executor.
     * @param shutdownCallbackExecutor if true, the callback executor will be shut down when
     *          this factory is shut down
     */
    public HttpClientFactory(ExecutorService callbackExecutor, boolean shutdownCallbackExecutor) {
        this(FilterChains.empty(),
                new NioClientSocketChannelFactory(
                        Executors.newCachedThreadPool(new NamedThreadFactory("R2 Netty IO Boss")),
                        Executors.newCachedThreadPool(new NamedThreadFactory("R2 Netty IO Worker"))),
                true, Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("R2 Netty Scheduler")),
                true, callbackExecutor, shutdownCallbackExecutor);
    }

    /**
     * Construct a new instance using the specified filter chain.
     *
     * @param filters the {@link FilterChain} shared by all Clients created by this factory.
     */
    public HttpClientFactory(FilterChain filters) {
        // TODO Disable Netty's thread renaming so that the names below are the ones that actually
        // show up in log messages; need to coordinate with Espresso team (who also have netty threads)
        this(filters,
                new NioClientSocketChannelFactory(
                        Executors.newCachedThreadPool(new NamedThreadFactory("R2 Netty IO Boss")),
                        Executors.newCachedThreadPool(new NamedThreadFactory("R2 Netty IO Worker"))),
                true, Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("R2 Netty Scheduler")),
                true);
    }

    /**
     * Creates a new HttpClientFactory.
     *
     * @param filters the filter chain shared by all Clients created by this factory
     * @param channelFactory the ClientSocketChannelFactory that all Clients created by this
     *          factory will share
     * @param shutdownFactory if true, the channelFactory will be shut down when this
     *          factory is shut down
     * @param executor an executor shared by all Clients created by this factory to schedule
     *          tasks
     * @param shutdownExecutor if true, the executor will be shut down when this factory is
     *          shut down
     */
    public HttpClientFactory(FilterChain filters, ClientSocketChannelFactory channelFactory,
            boolean shutdownFactory, ScheduledExecutorService executor, boolean shutdownExecutor) {
        this(filters, channelFactory, shutdownFactory, executor, shutdownExecutor, executor, false);
    }

    /**
     * Creates a new HttpClientFactory.
     *
     * @param filters the filter chain shared by all Clients created by this factory
     * @param channelFactory the ClientSocketChannelFactory that all Clients created by this
     *          factory will share
     * @param shutdownFactory if true, the channelFactory will be shut down when this
     *          factory is shut down
     * @param executor an executor shared by all Clients created by this factory to schedule
     *          tasks
     * @param shutdownExecutor if true, the executor will be shut down when this factory is
     *          shut down
     * @param callbackExecutor an optional executor to invoke user callbacks that otherwise
     *          will be invoked by scheduler executor.
     * @param shutdownCallbackExecutor if true, the callback executor will be shut down when
     *          this factory is shut down
     */
    public HttpClientFactory(FilterChain filters, ClientSocketChannelFactory channelFactory,
            boolean shutdownFactory, ScheduledExecutorService executor, boolean shutdownExecutor,
            ExecutorService callbackExecutor, boolean shutdownCallbackExecutor) {
        this(filters, channelFactory, shutdownFactory, executor, shutdownExecutor, callbackExecutor,
                shutdownCallbackExecutor, NULL_JMX_MANAGER);
    }

    public HttpClientFactory(FilterChain filters, ClientSocketChannelFactory channelFactory,
            boolean shutdownFactory, ScheduledExecutorService executor, boolean shutdownExecutor,
            ExecutorService callbackExecutor, boolean shutdownCallbackExecutor, AbstractJmxManager jmxManager) {
        _filters = filters;
        _channelFactory = channelFactory;
        _shutdownFactory = shutdownFactory;
        _executor = executor;
        _shutdownExecutor = shutdownExecutor;
        _callbackExecutor = callbackExecutor;
        _shutdownCallbackExecutor = shutdownCallbackExecutor;
        _jmxManager = jmxManager;
    }

    @Override
    public TransportClient getClient(Map<String, ? extends Object> properties) {
        SSLContext sslContext;
        SSLParameters sslParameters;

        // Copy the properties map since we don't want to mutate the passed-in map by removing keys
        properties = new HashMap<String, Object>(properties);
        sslContext = coerceAndRemoveFromMap(HTTP_SSL_CONTEXT, properties, SSLContext.class);
        sslParameters = coerceAndRemoveFromMap(HTTP_SSL_PARAMS, properties, SSLParameters.class);

        return getClient(properties, sslContext, sslParameters);
    }

    HttpNettyClient getRawClient(Map<String, String> properties) {
        return getRawClient(properties, null, null);
    }

    private static <T> T coerceAndRemoveFromMap(String key, Map<String, ?> props, Class<T> valueClass) {
        return coerce(key, props.remove(key), valueClass);
    }

    private static <T> T coerce(String key, Object value, Class<T> valueClass) {
        if (value == null) {
            return null;
        }
        if (!valueClass.isInstance(value)) {
            throw new IllegalArgumentException("Property " + key + " is of type " + value.getClass().getName()
                    + " but must be " + valueClass.getName());
        }
        return valueClass.cast(value);
    }

    /**
     * Create a new {@link TransportClient} with the specified properties,
     * {@link SSLContext} and {@link SSLParameters}
     *
     * @param properties map of properties for the {@link TransportClient}
     * @param sslContext {@link SSLContext} to be used for requests over SSL/TLS.
     * @param sslParameters {@link SSLParameters} to configure secure connections.
     * @return an appropriate {@link TransportClient} instance, as specified by the properties.
     */
    private TransportClient getClient(Map<String, ? extends Object> properties, SSLContext sslContext,
            SSLParameters sslParameters) {
        LOG.info("Getting a client with configuration {} and SSLContext {}", properties, sslContext);
        TransportClient client = getRawClient(properties, sslContext, sslParameters);

        List<String> httpResponseCompressionOperations = ConfigValueExtractor
                .buildList(properties.remove(HTTP_RESPONSE_COMPRESSION_OPERATIONS), LIST_SEPARATOR);

        FilterChain filters;
        if (!httpResponseCompressionOperations.isEmpty()) {
            String requestCompressionSchemaName = buildRequestEncodingSchemaName();
            String responseCompressionSchemaName = buildAcceptEncodingSchemaNames();
            filters = _filters.addLast(new ClientCompressionFilter(requestCompressionSchemaName,
                    responseCompressionSchemaName, httpResponseCompressionOperations));
        } else {
            filters = _filters;
        }

        client = new FilterChainClient(client, filters);
        client = new FactoryClient(client);
        synchronized (_mutex) {
            if (!_running) {
                throw new IllegalStateException("Factory is shutting down");
            }
            _clientsOutstanding++;
            return client;
        }
    }

    /**
     * @return the compression schema name used to compress the request to the server
     */
    private String buildRequestEncodingSchemaName() {
        return ""; // no request encoding for now
    }

    /**
     * @return the compression schemas that the client will support for response compression
     */
    private String buildAcceptEncodingSchemaNames() {
        List<String> schemaNames = new ArrayList<String>();
        for (EncodingType type : EncodingType.values()) {
            // For now clients will accept all supported encodings (which is why we don't add EncodingType.ANY as an accepted
            // type)
            if (!type.equals(EncodingType.IDENTITY) && !type.equals(EncodingType.ANY)) {
                schemaNames.add(type.getHttpName());
            }
        }
        return StringUtils.join(schemaNames, ",");
    }

    /**
     * helper method to get value from properties as well as to print log warning if the key is old
     * @param properties
     * @param propertyKey
     * @return null if property key can't be found, integer otherwise
     */
    private Integer getIntValue(Map<String, ? extends Object> properties, String propertyKey) {
        if (properties == null) {
            LOG.warn("passed a null raw client properties");
            return null;
        }
        if (properties.containsKey(propertyKey)) {
            // These properties can be safely cast to String before converting them to Integers as we expect Integer values
            // for all these properties.
            return Integer.parseInt((String) properties.get(propertyKey));
        } else {
            return null;
        }
    }

    private AsyncPoolImpl.Strategy getStrategy(Map<String, ? extends Object> properties) {
        if (properties == null) {
            LOG.warn("passed a null raw client properties");
            return null;
        }
        if (properties.containsKey(HTTP_POOL_STRATEGY)) {
            String strategyString = (String) properties.get(HTTP_POOL_STRATEGY);
            if (strategyString.equalsIgnoreCase("LRU")) {
                return AsyncPoolImpl.Strategy.LRU;
            } else if (strategyString.equalsIgnoreCase("MRU")) {
                return AsyncPoolImpl.Strategy.MRU;
            }
        }
        // for all other cases
        return null;
    }

    /**
     * Testing aid.
     */
    HttpNettyClient getRawClient(Map<String, ? extends Object> properties, SSLContext sslContext,
            SSLParameters sslParameters) {
        Integer poolSize = chooseNewOverDefault(getIntValue(properties, HTTP_POOL_SIZE), DEFAULT_POOL_SIZE);
        Integer idleTimeout = chooseNewOverDefault(getIntValue(properties, HTTP_IDLE_TIMEOUT),
                DEFAULT_IDLE_TIMEOUT);
        Integer shutdownTimeout = chooseNewOverDefault(getIntValue(properties, HTTP_SHUTDOWN_TIMEOUT),
                DEFAULT_SHUTDOWN_TIMEOUT);
        Integer maxResponseSize = chooseNewOverDefault(getIntValue(properties, HTTP_MAX_RESPONSE_SIZE),
                DEFAULT_MAX_RESPONSE_SIZE);
        Integer queryPostThreshold = chooseNewOverDefault(getIntValue(properties, HTTP_QUERY_POST_THRESHOLD),
                Integer.MAX_VALUE);
        Integer requestTimeout = chooseNewOverDefault(getIntValue(properties, HTTP_REQUEST_TIMEOUT),
                DEFAULT_REQUEST_TIMEOUT);
        Integer poolWaiterSize = chooseNewOverDefault(getIntValue(properties, HTTP_POOL_WAITER_SIZE),
                DEFAULT_POOL_WAITER_SIZE);
        String clientName = null;
        if (properties != null && properties.containsKey(HTTP_SERVICE_NAME)) {
            clientName = (String) properties.get(HTTP_SERVICE_NAME) + "Client";
        }
        clientName = chooseNewOverDefault(clientName, DEFAULT_CLIENT_NAME);
        AsyncPoolImpl.Strategy strategy = chooseNewOverDefault(getStrategy(properties), DEFAULT_POOL_STRATEGY);
        Integer poolMinSize = chooseNewOverDefault(getIntValue(properties, HTTP_POOL_MIN_SIZE),
                DEFAULT_POOL_MIN_SIZE);

        return new HttpNettyClient(_channelFactory, _executor, poolSize, requestTimeout, idleTimeout,
                shutdownTimeout, maxResponseSize, sslContext, sslParameters, queryPostThreshold, _callbackExecutor,
                poolWaiterSize, clientName, _jmxManager, strategy, poolMinSize);
    }

    /**
     * choose new value. If new value doesn't exist, choose default value.
     *
     * @param newValue
     * @param defaultValue
     */
    private <T> T chooseNewOverDefault(T newValue, T defaultValue) {
        if (newValue == null) {
            return defaultValue;
        } else {
            return newValue;
        }
    }

    /**
     * Initiates an orderly shutdown of the factory wherein no more clients will be created,
     * and the shutdown will complete when all existing clients have been shut down.  If some
     * clients fail to shutdown, the factory will never shut down.  Shutdown of the clients must
     * be initiated independently, but can occur before or after factory shutdown is initiated.
     *
     * After all clients have shut down, the ClientSocketChannelFactory and ScheduledExecutorService
     * will be shut down, if these options were selected at construction time.
     *
     * @param callback invoked after all outstanding clients and this factory have completed shutdown
     */
    @Override
    public void shutdown(final Callback<None> callback) {
        final int count;
        synchronized (_mutex) {
            _running = false;
            count = _clientsOutstanding;
            _factoryShutdownCallback = callback;
        }

        if (count == 0) {
            finishShutdown();
        } else {
            LOG.info("Awaiting shutdown of {} outstanding clients", count);
        }
    }

    /**
     * Initiates an orderly shutdown similar to
     * {@link #shutdown(com.ctriposs.common.callback.Callback)}. However, in the case that
     * some clients fail to shutdown, the factory shutdown will still complete after the
     * specified timeout.
     *
     * @param callback invoked after all clients shutdown (or the timeout expires) and the
     *          factory has shut down
     * @param timeout the timeout
     * @param timeoutUnit the timeout unit
     */
    public void shutdown(Callback<None> callback, long timeout, TimeUnit timeoutUnit) {
        // Schedule a timeout in case shutdown does not happen normally
        _shutdownTimeoutTask = _executor.schedule(new Runnable() {
            @Override
            public void run() {
                LOG.warn("Shutdown timeout exceeded, proceeding with shutdown");
                finishShutdown();
            }
        }, timeout, timeoutUnit);

        // Initiate orderly shutdown
        shutdown(callback);
    }

    private void finishShutdown() {
        if (!_finishingShutdown.compareAndSet(false, true)) {
            return;
        }
        if (_shutdownTimeoutTask != null) {
            _shutdownTimeoutTask.cancel(false);
        }

        // Under some circumstances, this method will be executed on a Netty IO thread.  For example,
        // as the factory waits for clients to shutdown, if the final client shuts down due an IO
        // event (receives response, connection refused, etc.).  In that case, the call to
        // releaseExternalResources() below will throw an exception -- because
        // releaseExternalResources() blocks until all threads are shut down, it refuses to run
        // if called from one of its own threads.  Therefore, schedule this task to run on
        // a different thread pool.  That does mean _executor will be shut down from one of its
        // own threads, but since this call doesn't await termination it's OK.
        _executor.execute(new Runnable() {
            @Override
            public void run() {
                if (_shutdownFactory) {
                    _channelFactory.releaseExternalResources();
                    LOG.info("ChannelFactory shutdown complete");
                }
                if (_shutdownExecutor) {
                    // Due to a bug in ScheduledThreadPoolExecutor, shutdownNow() returns cancelled
                    // tasks as though they were still pending execution.  If the executor has a large
                    // number of cancelled tasks, shutdownNow() could take a long time to copy the array
                    // of tasks.  Calling shutdown() first will purge the cancelled tasks.  Bug filed with
                    // Oracle; will provide bug number when available.  May be fixed in JDK7 already.
                    _executor.shutdown();
                    _executor.shutdownNow();
                    LOG.info("Scheduler shutdown complete");
                }
                if (_shutdownCallbackExecutor) {
                    _callbackExecutor.shutdown();
                    _callbackExecutor.shutdownNow();
                    LOG.info("Callback Executor shutdown complete");
                }
                final Callback<None> callback;
                synchronized (_mutex) {
                    callback = _factoryShutdownCallback;
                }
                LOG.info("Shutdown complete");
                callback.onSuccess(None.none());
            }
        });
    }

    private void clientShutdown() {
        final boolean done;
        synchronized (_mutex) {
            _clientsOutstanding--;
            done = !_running && _clientsOutstanding == 0;
        }
        if (done) {
            finishShutdown();
        }
    }

    /**
     * The FactoryClient is a wrapper that simply does reference counting for all clients
     * issued by this factory, so that we can know when all outstanding clients have been
     * shut down completely.
     *
     * It introduces no synchronization overhead in the per-request code path, only the
     * shutdown code path.
     */
    private class FactoryClient implements TransportClient {
        private final TransportClient _client;

        private FactoryClient(TransportClient client) {
            _client = client;
        }

        @Override
        public void restRequest(RestRequest request, RequestContext requestContext, Map<String, String> wireAttrs,
                TransportCallback<RestResponse> callback) {
            _client.restRequest(request, requestContext, wireAttrs, callback);
        }

        @Override
        public void shutdown(final Callback<None> callback) {
            _client.shutdown(new Callback<None>() {
                @Override
                public void onSuccess(None none) {
                    try {
                        callback.onSuccess(none);
                    } finally {
                        clientShutdown();
                    }
                }

                @Override
                public void onError(Throwable e) {
                    try {
                        callback.onError(e);
                    } finally {
                        clientShutdown();
                    }
                }
            });
        }
    }
}