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

com.netflix.client.netty.http.NettyHttpClient Maven / Gradle / Ivy

/*
 *
 * 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.client.netty.http;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelOption;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.reactivex.netty.channel.ObservableConnection;
import io.reactivex.netty.client.ClientMetricsEvent;
import io.reactivex.netty.client.CompositePoolLimitDeterminationStrategy;
import io.reactivex.netty.client.RxClient;
import io.reactivex.netty.contexts.ContextPipelineConfigurators;
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.PipelineConfigurators;
import io.reactivex.netty.pipeline.ssl.DefaultFactories;
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.protocol.text.sse.ServerSentEvent;
import io.reactivex.netty.servo.http.HttpClientListener;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.annotation.Nullable;

import rx.Observable;
import rx.Subscription;

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.netty.LoadBalancingRxClientWithPoolOptions;
import com.netflix.client.ssl.AbstractSslContextFactory;
import com.netflix.client.ssl.ClientSslSocketFactoryException;
import com.netflix.client.ssl.URLSslContextFactory;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.LoadBalancerBuilder;
import com.netflix.loadbalancer.LoadBalancerExecutor;
import com.netflix.loadbalancer.LoadBalancerObservableCommand;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerStats;

/**
 * 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 NettyHttpClient extends LoadBalancingRxClientWithPoolOptions, HttpClientResponse, HttpClient>
        implements HttpClient {
    protected static final PipelineConfigurator, HttpClientRequest> DEFAULT_PIPELINE_CONFIGURATOR = 
            PipelineConfigurators.httpClientConfigurator();
    protected static final PipelineConfigurator, HttpClientRequest> DEFAULT_SSE_PIPELINE_CONFIGURATOR = 
            PipelineConfigurators.sseClientConfigurator();
    
    private String requestIdHeaderName;
    
    private HttpRequestIdProvider requestIdProvider;
    
    public NettyHttpClient(ILoadBalancer lb, PipelineConfigurator, HttpClientRequest> pipeLineConfigurator,
            ScheduledExecutorService poolCleanerScheduler) {
        this(lb, DefaultClientConfigImpl.getClientConfigWithDefaultValues(), 
                new NettyHttpLoadBalancerErrorHandler(), pipeLineConfigurator, poolCleanerScheduler);
    }
    
    public NettyHttpClient(
            IClientConfig config,
            RetryHandler retryHandler,
            PipelineConfigurator, HttpClientRequest> pipelineConfigurator,
            ScheduledExecutorService poolCleanerScheduler) {
        this(LoadBalancerBuilder.newBuilder().withClientConfig(config).buildLoadBalancerFromConfigWithReflection(),
                config, retryHandler, pipelineConfigurator, poolCleanerScheduler);
    }

    public NettyHttpClient(
            ILoadBalancer lb,
            IClientConfig config,
            RetryHandler retryHandler,
            PipelineConfigurator, HttpClientRequest> pipelineConfigurator,
            ScheduledExecutorService poolCleanerScheduler) {
        super(lb, config, retryHandler, pipelineConfigurator, poolCleanerScheduler);
        requestIdHeaderName = getProperty(IClientConfigKey.Keys.RequestIdHeaderName, null, null);
        if (requestIdHeaderName != null) {
            requestIdProvider = new HttpRequestIdProvider(requestIdHeaderName, RxContexts.DEFAULT_CORRELATOR);
        }
    }

    private RequestSpecificRetryHandler getRequestRetryHandler(HttpClientRequest request, IClientConfig requestConfig) {
        boolean okToRetryOnAllErrors = request.getMethod().equals(HttpMethod.GET);
        return new RequestSpecificRetryHandler(true, okToRetryOnAllErrors, lbExecutor.getRetryHandler(), requestConfig);
    }
        
    protected void setHost(HttpClientRequest request, String host) {
        request.getHeaders().set(HttpHeaders.Names.HOST, host);
    }

    public Observable> submit(String host, int port, final HttpClientRequest request) {
        return submit(host, port, request, getRxClientConfig(null));
    }
       
    public Observable> submit(String host, int port, final HttpClientRequest request, ClientConfig rxClientConfig) {
        Preconditions.checkNotNull(host);
        Preconditions.checkNotNull(request);
        HttpClient rxClient = getRxClient(host, port);
        setHost(request, host);
        return rxClient.submit(request, rxClientConfig);
    }
    
    private RxClient.ClientConfig getRxClientConfig(IClientConfig requestConfig) {
        if (requestConfig == null) {
            return HttpClientConfig.Builder.newDefaultConfig();
        }
        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();        
    }
    
    public Observable> submit(String host, int port, final HttpClientRequest request, @Nullable final IClientConfig requestConfig) {
        return submit(host, port, request, 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) {
        final RetryHandler retryHandler = (errorHandler == null) ? getRequestRetryHandler(request, requestConfig) : errorHandler;
        final ClientConfig rxClientConfig = getRxClientConfig(requestConfig);
        Observable> result = submitToServerInURI(request, rxClientConfig, retryHandler);
        if (result != null) {
            return result;
        }
        return lbExecutor.create(new LoadBalancerObservableCommand>() {
            @Override
            public Observable> run(
                    Server server) {
                return submit(server.getHost(), server.getPort(), request, rxClientConfig);
            }
        }, retryHandler);
    }
    
    @VisibleForTesting
    ServerStats getServerStats(Server server) {
        return lbExecutor.getServerStats(server);
    }

    /**
     * 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) {
        final RetryHandler retryHandler = getRequestRetryHandler(request, null);
        Observable> result = submitToServerInURI(request, config, retryHandler);
        if (result != null) {
            return result;
        }
        return lbExecutor.create(new LoadBalancerObservableCommand>() {
            @Override
            public Observable> run(
                    Server server) {
                return submit(server.getHost(), server.getPort(), request, config);
            }
        }, retryHandler);
    }

    private Observable> submitToServerInURI(HttpClientRequest request, ClientConfig config, RetryHandler errorHandler)  {
        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 (clientConfig.getPropertyAsBoolean(IClientConfigKey.Keys.IsSecure, false)) {
                port = 443;
            } else {
                port = 80;
            }
        }
        if (errorHandler.getMaxRetriesOnSameServer() == 0) {
            return submit(host, port, request, config);
        }
        Server server = new Server(host, port);
        return lbExecutor.retryWithSameServer(server, submit(server.getHost(), server.getPort(), request, config), errorHandler);
    }
    
    /**
     * Create an {@link ObservableConnection} with a server chosen by the load balancer. 
     */
    @Override
    public Observable, HttpClientRequest>> connect() {
        return lbExecutor.create(new LoadBalancerObservableCommand, HttpClientRequest>>() {
            @Override
            public Observable, HttpClientRequest>> run(
                    Server server) {
                HttpClient rxClient = getRxClient(server.getHost(), server.getPort());
                return rxClient.connect();
            }
        });
    }

    @Override
    protected HttpClient cacheLoadRxClient(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);
        }
        RxClient.ClientConfig rxClientConfig = builder.build();
        clientBuilder.channelOption(
                ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout)
                .config(rxClientConfig);
        if (isPoolEnabled()) {
            clientBuilder.withConnectionPoolLimitStrategy(poolStrategy)
            .withIdleConnectionsTimeoutMillis(idleConnectionEvictionMills)
            .withPoolIdleCleanupScheduler(poolCleanerScheduler);
        } else {
            clientBuilder.withNoConnectionPooling();
        }
        if (sslContextFactory != null) {
            try {
                clientBuilder.withSslEngineFactory(DefaultFactories.fromSSLContext(sslContextFactory.getSSLContext()));
            } catch (ClientSslSocketFactoryException e) {
                throw new RuntimeException(e);
            }
        }
        HttpClient client = clientBuilder.build();
        return client;
    }
    
    HttpClientListener getListener() {
        return (HttpClientListener) listener;
    }

    LoadBalancerExecutor getLoadBalancerExecutor() {
        return lbExecutor;
    }
    
    @Override
    protected MetricEventsListener> createListener(
            String name) {
        return HttpClientListener.newHttpListener(name);
    }

}