com.netflix.ribbon.transport.netty.http.LoadBalancingHttpClient 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.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 extends ClientMetricsEvent>> createListener(String name) {
return HttpClientListener.newHttpListener(name);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy