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

com.netflix.ribbon.transport.netty.LoadBalancingRxClient 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;

import io.reactivex.netty.channel.ObservableConnection;
import io.reactivex.netty.client.ClientMetricsEvent;
import io.reactivex.netty.client.RxClient;
import io.reactivex.netty.metrics.MetricEventsListener;
import io.reactivex.netty.metrics.MetricEventsSubject;
import io.reactivex.netty.pipeline.PipelineConfigurator;

import java.net.URL;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import rx.Observable;
import rx.Subscription;

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.AbstractSslContextFactory;
import com.netflix.client.ssl.ClientSslSocketFactoryException;
import com.netflix.client.ssl.URLSslContextFactory;
import com.netflix.client.util.Resources;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.LoadBalancerBuilder;
import com.netflix.loadbalancer.LoadBalancerContext;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerListChangeListener;
import com.netflix.loadbalancer.reactive.LoadBalancerCommand;
import com.netflix.loadbalancer.reactive.ServerOperation;

/**
 * Decorator for RxClient which adds load balancing functionality.  This implementation uses
 * an ILoadBlanacer and caches the mapping from Server to a client implementation
 * 
 * @author elandau
 *
 * @param    Client input type
 * @param    Client output type
 * @param    Specific RxClient derived type 
 */
public abstract class LoadBalancingRxClient> implements RxClient {

    private static final Logger logger = LoggerFactory.getLogger(LoadBalancingRxClient.class);
    
    protected final ConcurrentMap rxClientCache;
    protected final PipelineConfigurator pipelineConfigurator;
    protected final IClientConfig clientConfig;
    protected final RetryHandler defaultRetryHandler;
    protected final AbstractSslContextFactory sslContextFactory;
    protected final MetricEventsListener> listener;
    protected final MetricEventsSubject> eventSubject;
    protected final LoadBalancerContext lbContext;

    public LoadBalancingRxClient(IClientConfig config, RetryHandler defaultRetryHandler, PipelineConfigurator pipelineConfigurator) {
        this(LoadBalancerBuilder.newBuilder().withClientConfig(config).buildLoadBalancerFromConfigWithReflection(),
                config,
                defaultRetryHandler,
                pipelineConfigurator
                );
    }
    
    public LoadBalancingRxClient(ILoadBalancer lb, IClientConfig config, RetryHandler defaultRetryHandler, PipelineConfigurator pipelineConfigurator) {
        this.rxClientCache = new ConcurrentHashMap();
        this.lbContext = new LoadBalancerContext(lb, config, defaultRetryHandler);
        this.defaultRetryHandler = defaultRetryHandler;
        this.pipelineConfigurator = pipelineConfigurator;
        this.clientConfig = config;
        this.listener = createListener(config.getClientName());
        
        eventSubject = new MetricEventsSubject>();
        boolean isSecure = getProperty(IClientConfigKey.Keys.IsSecure, null, false); 
        if (isSecure) {
            final URL trustStoreUrl = getResourceForOptionalProperty(CommonClientConfigKey.TrustStore);
            final URL keyStoreUrl = getResourceForOptionalProperty(CommonClientConfigKey.KeyStore);
            boolean isClientAuthRequired = clientConfig.get(IClientConfigKey.Keys.IsClientAuthRequired, false);
            if (    // if client auth is required, need both a truststore and a keystore to warrant configuring
                    // if client is not is not required, we only need a keystore OR a truststore to warrant configuring
                    (isClientAuthRequired && (trustStoreUrl != null && keyStoreUrl != null))
                    ||
                    (!isClientAuthRequired && (trustStoreUrl != null || keyStoreUrl != null))
                    ) {

                try {
                    sslContextFactory = new URLSslContextFactory(trustStoreUrl,
                            clientConfig.get(CommonClientConfigKey.TrustStorePassword),
                            keyStoreUrl,
                            clientConfig.get(CommonClientConfigKey.KeyStorePassword));

                } catch (ClientSslSocketFactoryException e) {
                    throw new IllegalArgumentException("Unable to configure custom secure socket factory", e);
                }
            } else {
                sslContextFactory = null;
            }
        } else {
            sslContextFactory = null;
        }

        addLoadBalancerListener();
    }
      
    public IClientConfig getClientConfig() {
        return clientConfig;
    }

    public int getResponseTimeOut() {
        int maxRetryNextServer = 0;
        int maxRetrySameServer = 0;
        if (defaultRetryHandler != null) {
            maxRetryNextServer = defaultRetryHandler.getMaxRetriesOnNextServer();
            maxRetrySameServer = defaultRetryHandler.getMaxRetriesOnSameServer();
        } else {
            maxRetryNextServer = clientConfig.get(IClientConfigKey.Keys.MaxAutoRetriesNextServer, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER);
            maxRetrySameServer = clientConfig.get(IClientConfigKey.Keys.MaxAutoRetries, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES);
        }
        int readTimeout = getProperty(IClientConfigKey.Keys.ReadTimeout, null, DefaultClientConfigImpl.DEFAULT_READ_TIMEOUT);
        int connectTimeout = getProperty(IClientConfigKey.Keys.ConnectTimeout, null, DefaultClientConfigImpl.DEFAULT_CONNECT_TIMEOUT);
        return (maxRetryNextServer + 1) * (maxRetrySameServer + 1) * (readTimeout + connectTimeout);
    }
    
    public int getMaxConcurrentRequests() {
        return -1;
    }
        
    /**
     * Resolve the final property value from,
     * 1. Request specific configuration
     * 2. Default configuration
     * 3. Default value
     * 
     * @param key
     * @param requestConfig
     * @param defaultValue
     * @return
     */
    protected  S getProperty(IClientConfigKey key, @Nullable IClientConfig requestConfig, S defaultValue) {
        if (requestConfig != null && requestConfig.get(key) != null) {
            return requestConfig.get(key);
        } else {
            return clientConfig.get(key, defaultValue);
        }
    }

    protected URL getResourceForOptionalProperty(final IClientConfigKey configKey) {
        final String propValue = clientConfig.get(configKey);
        URL result = null;

        if (propValue != null) {
            result = Resources.getResource(propValue);
            if (result == null) {
                throw new IllegalArgumentException("No resource found for " + configKey + ": "
                        + propValue);
            }
        }
        return result;
    }

    /**
     * Add a listener that is responsible for removing an HttpClient and shutting down
     * its connection pool if it is no longer available from load balancer.
     */
    private void addLoadBalancerListener() {
        if (!(lbContext.getLoadBalancer() instanceof BaseLoadBalancer)) {
            return;
        }
        
        ((BaseLoadBalancer) lbContext.getLoadBalancer()).addServerListChangeListener(new ServerListChangeListener() {
            @Override
            public void serverListChanged(List oldList, List newList) {
                Set removedServers = new HashSet(oldList);
                removedServers.removeAll(newList);
                for (Server server: rxClientCache.keySet()) {
                    if (removedServers.contains(server)) {
                        // this server is no longer in UP status
                        removeClient(server);
                    }
                }
            }
        });
    }

    /**
     * Create a client instance for this Server.  Note that only the client object is created
     * here but that the client connection is not created yet.
     * 
     * @param server
     * @return
     */
    protected abstract T createRxClient(Server server);
    
    /**
     * Look up the client associated with this Server.
     * @param host
     * @param port
     * @return
     */
    protected T getOrCreateRxClient(Server server) {
        T client =  rxClientCache.get(server);
        if (client != null) {
            return client;
        } 
        else {
            client = createRxClient(server);
            client.subscribe(listener);
            client.subscribe(eventSubject);
            T old = rxClientCache.putIfAbsent(server, client);
            if (old != null) {
                return old;
            } else {
                return client;
            }
        }
    }
    
    /**
     * Remove the client for this Server
     * @param server
     * @return The RxClient implementation or null if not found
     */
    protected T removeClient(Server server) {
        T client = rxClientCache.remove(server);
        if (client != null) {
            client.shutdown();
        }
        return client;
    }
    
    @Override
    public Observable> connect() {
        return LoadBalancerCommand.>builder()
                .withLoadBalancerContext(lbContext)
                .build()
                .submit(new ServerOperation>() {
                    @Override
                    public Observable> call(Server server) {
                        return getOrCreateRxClient(server).connect();            
                    }                    
                });
    }

    protected abstract MetricEventsListener> createListener(String name);
    
    @Override
    public void shutdown() {
        for (Server server: rxClientCache.keySet()) {
            removeClient(server);
        }
    }

    @Override
    public String name() {
        return clientConfig.getClientName();
    }

    @Override
    public Subscription subscribe(MetricEventsListener> listener) {
       return eventSubject.subscribe(listener);
    }

    public final LoadBalancerContext getLoadBalancerContext() {
        return lbContext;
    }
}