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

com.netflix.ribbon.transport.netty.http.LoadBalancingHttpClient Maven / Gradle / Ivy

There is a newer version: 2.7.18
Show newest version
/*
 *
 * Copyright 2014 Netflix, Inc.
 *
 * 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.netflix.ribbon.transport.netty.http;

import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelOption;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.reactivex.netty.client.ClientMetricsEvent;
import io.reactivex.netty.client.CompositePoolLimitDeterminationStrategy;
import io.reactivex.netty.client.RxClient;
import io.reactivex.netty.contexts.RxContexts;
import io.reactivex.netty.contexts.http.HttpRequestIdProvider;
import io.reactivex.netty.metrics.MetricEventsListener;
import io.reactivex.netty.pipeline.PipelineConfigurator;
import io.reactivex.netty.pipeline.ssl.DefaultFactories;
import io.reactivex.netty.pipeline.ssl.SSLEngineFactory;
import io.reactivex.netty.protocol.http.client.HttpClient;
import io.reactivex.netty.protocol.http.client.HttpClientBuilder;
import io.reactivex.netty.protocol.http.client.HttpClientRequest;
import io.reactivex.netty.protocol.http.client.HttpClientResponse;
import io.reactivex.netty.servo.http.HttpClientListener;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.net.ssl.SSLEngine;

import rx.Observable;
import rx.functions.Func1;
import rx.functions.Func2;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.netflix.client.RequestSpecificRetryHandler;
import com.netflix.client.RetryHandler;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.DefaultClientConfigImpl;
import com.netflix.client.config.IClientConfig;
import com.netflix.client.config.IClientConfigKey;
import com.netflix.client.ssl.ClientSslSocketFactoryException;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.LoadBalancerBuilder;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerStats;
import com.netflix.loadbalancer.reactive.ExecutionContext;
import com.netflix.loadbalancer.reactive.ExecutionListener;
import com.netflix.loadbalancer.reactive.LoadBalancerCommand;
import com.netflix.loadbalancer.reactive.ServerOperation;
import com.netflix.ribbon.transport.netty.LoadBalancingRxClientWithPoolOptions;

/**
 * A Netty HttpClient that can connect to different servers. Internally it caches the RxNetty's HttpClient, with each created with 
 * a connection pool governed by {@link CompositePoolLimitDeterminationStrategy} that has a global limit and per server limit. 
 *   
 * @author awang
 */
public class LoadBalancingHttpClient extends LoadBalancingRxClientWithPoolOptions, HttpClientResponse, HttpClient>
        implements HttpClient {

    private static final HttpClientConfig DEFAULT_RX_CONFIG = HttpClientConfig.Builder.newDefaultConfig();
    
    private final String requestIdHeaderName;
    private final HttpRequestIdProvider requestIdProvider;
    private final List, HttpClientResponse>> listeners;
    private final LoadBalancerCommand> defaultCommandBuilder;
    private final Func2, Integer, Observable>> responseToErrorPolicy;
    private final Func1 backoffStrategy;
    
    public static class Builder {
        ILoadBalancer lb;
        IClientConfig config;
        RetryHandler retryHandler;
        PipelineConfigurator, HttpClientRequest> pipelineConfigurator;
        ScheduledExecutorService poolCleanerScheduler;
        List, HttpClientResponse>> listeners;
        Func2, Integer, Observable>> responseToErrorPolicy;
        Func1 backoffStrategy;
        Func1, LoadBalancingHttpClient> build;
        
        protected Builder(Func1, LoadBalancingHttpClient> build) {
            this.build = build;
        }
        
        public Builder withLoadBalancer(ILoadBalancer lb) {
            this.lb = lb;
            return this;
        }
        
        public Builder withClientConfig(IClientConfig config) {
            this.config = config;
            return this;
        }
        
        public Builder withRetryHandler(RetryHandler retryHandler) {
            this.retryHandler = retryHandler;
            return this;
        }
        
        public Builder withPipelineConfigurator(PipelineConfigurator, HttpClientRequest> pipelineConfigurator) {
            this.pipelineConfigurator = pipelineConfigurator;
            return this;
        }
        
        public Builder withPoolCleanerScheduler(ScheduledExecutorService poolCleanerScheduler) {
            this.poolCleanerScheduler = poolCleanerScheduler;
            return this;
        }
        
        public Builder withExecutorListeners(List, HttpClientResponse>> listeners) {
            this.listeners = listeners;
            return this;
        }
        
        /**
         * Policy for converting a response to an error if the status code indicates it as such.  This will only
         * be called for responses with status code 4xx or 5xx
         * 
         * Parameters to the function are
         * * HttpClientResponse - The actual response
         * * Integer - Backoff to apply if this is a retryable error.  The backoff amount is in milliseconds
         *             and is based on the configured BackoffStrategy.  It is the responsibility of this function
         *             to implement the actual backoff mechanism.  This can be done as Observable.error(e).delay(backoff, TimeUnit.MILLISECONDS)
         * The return Observable will either contain the HttpClientResponse if is it not an error or an 
         * Observable.error() with the translated exception.
         * 
         * @param responseToErrorPolicy
         */
        public Builder withResponseToErrorPolicy(Func2, Integer, Observable>> responseToErrorPolicy) {
            this.responseToErrorPolicy = responseToErrorPolicy;
            return this;
        }
        
        /**
         * Strategy for calculating the backoff based on the number of reties.  Input is the number
         * of retries and output is the backoff amount in milliseconds.
         * The default implementation is non random exponential backoff with time interval configurable
         * via the property BackoffInterval (default 1000 msec)
         * 
         * @param BackoffStrategy
         */
        public Builder withBackoffStrategy(Func1 backoffStrategy) {
            this.backoffStrategy = backoffStrategy;
            return this;
        }
        
        public LoadBalancingHttpClient build() {
            if (retryHandler == null) {
                retryHandler = new NettyHttpLoadBalancerErrorHandler();
            }
            if (config == null) {
                config = DefaultClientConfigImpl.getClientConfigWithDefaultValues();
            }
            if (lb == null) {
                lb = LoadBalancerBuilder.newBuilder().withClientConfig(config).buildLoadBalancerFromConfigWithReflection();
            }
            if (listeners == null) {
                listeners = Collections., HttpClientResponse>>emptyList();
            }
            if (backoffStrategy == null) {
                backoffStrategy = new Func1() {
                    @Override
                    public Integer call(Integer backoffCount) {
                        int interval = config.getOrDefault(IClientConfigKey.Keys.BackoffInterval);
                        if (backoffCount < 0) {
                            backoffCount = 0;
                        }
                        else if (backoffCount > 10) {   // Reasonable upper bound
                            backoffCount = 10;
                        }
                        return (int)Math.pow(2, backoffCount) * interval;
                    }
                };
            }
            if (responseToErrorPolicy == null) {
                responseToErrorPolicy = new DefaultResponseToErrorPolicy();
            }
            return build.call(this);
        }
    }
    
    public static  Builder builder() {
        return new Builder(new Func1, LoadBalancingHttpClient>() {
            @Override
            public LoadBalancingHttpClient call(Builder builder) {
                return new LoadBalancingHttpClient(builder);
            }
        });
    }
    
    protected LoadBalancingHttpClient(Builder builder) {
        super(builder.lb, builder.config, new RequestSpecificRetryHandler(true, true, builder.retryHandler, null), builder.pipelineConfigurator, builder.poolCleanerScheduler);
        requestIdHeaderName = getProperty(IClientConfigKey.Keys.RequestIdHeaderName, null, null);
        requestIdProvider = (requestIdHeaderName != null) 
                          ? new HttpRequestIdProvider(requestIdHeaderName, RxContexts.DEFAULT_CORRELATOR)
                          : null;
        this.listeners = new CopyOnWriteArrayList, HttpClientResponse>>(builder.listeners);
        defaultCommandBuilder = LoadBalancerCommand.>builder()
                .withLoadBalancerContext(lbContext)
                .withListeners(this.listeners)
                .withClientConfig(builder.config)
                .withRetryHandler(builder.retryHandler)
                .build();
        this.responseToErrorPolicy = builder.responseToErrorPolicy;
        this.backoffStrategy = builder.backoffStrategy;
    }

    private RetryHandler getRequestRetryHandler(HttpClientRequest request, IClientConfig requestConfig) {
        return new RequestSpecificRetryHandler(
                true, 
                request.getMethod().equals(HttpMethod.GET),     // Default only allows retrys for GET
                defaultRetryHandler, 
                requestConfig);
    }

    protected static void setHostHeader(HttpClientRequest request, String host) {
        request.getHeaders().set(HttpHeaders.Names.HOST, host);
    }

    /**
     * Submit a request to server chosen by the load balancer to execute. An error will be emitted from the returned {@link Observable} if 
     * there is no server available from load balancer.
     */
    @Override
    public Observable> submit(HttpClientRequest request) {
        return submit(request, null, null);
    }

    /**
     * Submit a request to server chosen by the load balancer to execute. An error will be emitted from the returned {@link Observable} if 
     * there is no server available from load balancer.
     * 
     * @param config An {@link ClientConfig} to override the default configuration for the client. Can be null.
     * @return
     */
    @Override
    public Observable> submit(final HttpClientRequest request, final ClientConfig config) {
        return submit(null, request, null, null, config);
    }

    /**
     * Submit a request to run on a specific server
     * 
     * @param server
     * @param request
     * @param requestConfig
     * @return
     */
    public Observable> submit(Server server, final HttpClientRequest request, final IClientConfig requestConfig) {
        return submit(server, request, null, requestConfig, getRxClientConfig(requestConfig));
    }

    /**
     * Submit a request to server chosen by the load balancer to execute. An error will be emitted from the returned {@link Observable} if 
     * there is no server available from load balancer.
     * 
     * @param errorHandler A handler to determine the load balancer retry logic. If null, the default one will be used.
     * @param requestConfig An {@link IClientConfig} to override the default configuration for the client. Can be null.
     * @return
     */
    public Observable> submit(final HttpClientRequest request, final RetryHandler errorHandler, final IClientConfig requestConfig) {
        return submit(null, request, errorHandler, requestConfig, null);
    }
    
    public Observable> submit(Server server, final HttpClientRequest request) {
        return submit(server, request, null, null, getRxClientConfig(null));
    }

    /**
     * Convert an HttpClientRequest to a ServerOperation 
     * 
     * @param server
     * @param request
     * @param rxClientConfig
     * @return
     */
    protected ServerOperation> requestToOperation(final HttpClientRequest request, final ClientConfig rxClientConfig) {
        Preconditions.checkNotNull(request);
        
        return new ServerOperation>() {
            final AtomicInteger count = new AtomicInteger(0);
            
            @Override
            public Observable> call(Server server) {
                HttpClient rxClient = getOrCreateRxClient(server);
                setHostHeader(request, server.getHost());
                
                Observable> o;
                if (rxClientConfig != null) {
                    o = rxClient.submit(request, rxClientConfig);
                } 
                else {
                    o = rxClient.submit(request);
                }
                
                return o.concatMap(new Func1, Observable>>() {
                    @Override
                    public Observable> call(HttpClientResponse t1) {
                        if (t1.getStatus().code()/100 == 4 || t1.getStatus().code()/100 == 5)
                            return responseToErrorPolicy.call(t1, backoffStrategy.call(count.getAndIncrement()));
                        else
                            return Observable.just(t1);
                    }
                });
            }
        };
    }
    
    /** 
     * Construct an RxClient.ClientConfig from an IClientConfig
     * 
     * @param requestConfig
     * @return
     */
    private RxClient.ClientConfig getRxClientConfig(IClientConfig requestConfig) {
        if (requestConfig == null) {
            return DEFAULT_RX_CONFIG;
        }
        int requestReadTimeout = getProperty(IClientConfigKey.Keys.ReadTimeout, requestConfig, 
                                             DefaultClientConfigImpl.DEFAULT_READ_TIMEOUT);
        Boolean followRedirect = getProperty(IClientConfigKey.Keys.FollowRedirects, requestConfig, null);
        HttpClientConfig.Builder builder = new HttpClientConfig.Builder().readTimeout(requestReadTimeout, TimeUnit.MILLISECONDS);
        if (followRedirect != null) {
            builder.setFollowRedirect(followRedirect);
        }
        return builder.build();        
    }

    /**
     * @return ClientConfig that is merged from IClientConfig and ClientConfig in the method arguments
     */
    private RxClient.ClientConfig getRxClientConfig(IClientConfig ribbonClientConfig, ClientConfig rxClientConfig) {
        if (ribbonClientConfig == null) {
            return rxClientConfig;
        } 
        else if (rxClientConfig == null) {
            return getRxClientConfig(ribbonClientConfig);
        }
        int readTimeoutFormRibbon = ribbonClientConfig.get(CommonClientConfigKey.ReadTimeout, -1);
        if (rxClientConfig instanceof HttpClientConfig) {
            HttpClientConfig httpConfig = (HttpClientConfig) rxClientConfig;
            HttpClientConfig.Builder builder = HttpClientConfig.Builder.from(httpConfig);
            if (readTimeoutFormRibbon >= 0) {
                builder.readTimeout(readTimeoutFormRibbon, TimeUnit.MILLISECONDS);
            }
            return builder.build();
        } 
        else {
            RxClient.ClientConfig.Builder builder = new RxClient.ClientConfig.Builder(rxClientConfig);
            if (readTimeoutFormRibbon >= 0) {
                builder.readTimeout(readTimeoutFormRibbon, TimeUnit.MILLISECONDS);
            }
            return builder.build();
        }
    }

    private IClientConfig getRibbonClientConfig(ClientConfig rxClientConfig) {
        if (rxClientConfig != null && rxClientConfig.isReadTimeoutSet()) {
            return IClientConfig.Builder.newBuilder().withReadTimeout((int) rxClientConfig.getReadTimeoutInMillis()).build();
        }
        return null;
    }

    /**
     * Subject an operation to run in the load balancer
     * 
     * @param request
     * @param errorHandler
     * @param requestConfig
     * @param rxClientConfig
     * @return
     */
    private Observable> submit(final Server server, final HttpClientRequest request, final RetryHandler errorHandler, final IClientConfig requestConfig, final ClientConfig rxClientConfig) {
        RetryHandler retryHandler = errorHandler;
        if (retryHandler == null) {
            retryHandler = getRequestRetryHandler(request, requestConfig);
        }
        
        final IClientConfig config = requestConfig == null ? DefaultClientConfigImpl.getEmptyConfig() : requestConfig;
        final ExecutionContext> context = new ExecutionContext>(request, config, this.getClientConfig(), retryHandler);
        
        Observable> result = submitToServerInURI(request, config, rxClientConfig, retryHandler, context);
        if (result == null) {
            LoadBalancerCommand> command;
            if (retryHandler != defaultRetryHandler) {
                // need to create new builder instead of the default one
                command = LoadBalancerCommand.>builder()
                        .withExecutionContext(context)
                        .withLoadBalancerContext(lbContext)
                        .withListeners(listeners)
                        .withClientConfig(this.getClientConfig())
                        .withRetryHandler(retryHandler)
                        .withServer(server)
                        .build();
            }
            else {
                command = defaultCommandBuilder;
            }
            
            result = command.submit(requestToOperation(request, getRxClientConfig(config, rxClientConfig)));
        }
        return result;
    }

    @VisibleForTesting
    ServerStats getServerStats(Server server) {
        return lbContext.getServerStats(server);
    }

    /**
     * Submits the request to the server indicated in the URI
     * @param request
     * @param requestConfig
     * @param config
     * @param errorHandler
     * @param context
     * @return
     */
    private Observable> submitToServerInURI(
            HttpClientRequest request, IClientConfig requestConfig, ClientConfig config,
            RetryHandler errorHandler, ExecutionContext> context)  {
        // First, determine server from the URI
        URI uri;
        try {
            uri = new URI(request.getUri());
        } catch (URISyntaxException e) {
            return Observable.error(e);
        }
        String host = uri.getHost();
        if (host == null) {
            return null;
        }
        int port = uri.getPort();
        if (port < 0) {
            if (Optional.ofNullable(clientConfig.get(IClientConfigKey.Keys.IsSecure)).orElse(false)) {
                port = 443;
            } else {
                port = 80;
            }
        }
        
        return LoadBalancerCommand.>builder()
                .withRetryHandler(errorHandler)
                .withLoadBalancerContext(lbContext)
                .withListeners(listeners)
                .withExecutionContext(context)
                .withServer(new Server(host, port))
                .build()
                .submit(this.requestToOperation(request, getRxClientConfig(requestConfig, config)));
    }
    
    @Override
    protected HttpClient createRxClient(Server server) {
        HttpClientBuilder clientBuilder;
        if (requestIdProvider != null) {
            clientBuilder = RxContexts.newHttpClientBuilder(server.getHost(), server.getPort(), 
                    requestIdProvider, RxContexts.DEFAULT_CORRELATOR, pipelineConfigurator);
        } else {
            clientBuilder = RxContexts.newHttpClientBuilder(server.getHost(), server.getPort(), 
                    RxContexts.DEFAULT_CORRELATOR, pipelineConfigurator);
        }
        Integer connectTimeout = getProperty(IClientConfigKey.Keys.ConnectTimeout,  null, DefaultClientConfigImpl.DEFAULT_CONNECT_TIMEOUT);
        Integer readTimeout    = getProperty(IClientConfigKey.Keys.ReadTimeout,     null, DefaultClientConfigImpl.DEFAULT_READ_TIMEOUT);
        Boolean followRedirect = getProperty(IClientConfigKey.Keys.FollowRedirects, null, null);
        HttpClientConfig.Builder builder = new HttpClientConfig.Builder().readTimeout(readTimeout, TimeUnit.MILLISECONDS);
        if (followRedirect != null) {
            builder.setFollowRedirect(followRedirect);
        }
        clientBuilder
                .channelOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout)
                .config(builder.build());
        if (isPoolEnabled()) {
            clientBuilder
                .withConnectionPoolLimitStrategy(poolStrategy)
                .withIdleConnectionsTimeoutMillis(idleConnectionEvictionMills)
                .withPoolIdleCleanupScheduler(poolCleanerScheduler);
        } 
        else {
            clientBuilder
                .withNoConnectionPooling();
        }
        
        if (sslContextFactory != null) {
            try {
                SSLEngineFactory myFactory = new DefaultFactories.SSLContextBasedFactory(sslContextFactory.getSSLContext()) {
                    @Override
                    public SSLEngine createSSLEngine(ByteBufAllocator allocator) {
                        SSLEngine myEngine = super.createSSLEngine(allocator);
                        myEngine.setUseClientMode(true);
                        return myEngine;
                    }
                };

                clientBuilder.withSslEngineFactory(myFactory);
            } catch (ClientSslSocketFactoryException e) {
                throw new RuntimeException(e);
            }
        }
        return clientBuilder.build();
    }
    
    @VisibleForTesting
    HttpClientListener getListener() {
        return (HttpClientListener) listener;
    }

    @VisibleForTesting
    Map> getRxClients() {
        return rxClientCache;
    }
    
    @Override
    protected MetricEventsListener> createListener(String name) {
        return HttpClientListener.newHttpListener(name);
    }

}