io.servicetalk.http.netty.DefaultSingleAddressHttpClientBuilder Maven / Gradle / Ivy
Show all versions of servicetalk-http-netty Show documentation
/*
* Copyright © 2018-2023 Apple Inc. and the ServiceTalk project authors
*
* 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 io.servicetalk.http.netty;
import io.servicetalk.buffer.api.BufferAllocator;
import io.servicetalk.buffer.api.CharSequences;
import io.servicetalk.client.api.ConnectionFactory;
import io.servicetalk.client.api.ConnectionFactoryFilter;
import io.servicetalk.client.api.DefaultServiceDiscovererEvent;
import io.servicetalk.client.api.DelegatingServiceDiscoverer;
import io.servicetalk.client.api.LoadBalancer;
import io.servicetalk.client.api.ServiceDiscoverer;
import io.servicetalk.client.api.ServiceDiscovererEvent;
import io.servicetalk.concurrent.CompletableSource;
import io.servicetalk.concurrent.CompletableSource.Subscriber;
import io.servicetalk.concurrent.api.BiIntFunction;
import io.servicetalk.concurrent.api.Completable;
import io.servicetalk.concurrent.api.CompositeCloseable;
import io.servicetalk.concurrent.api.Executor;
import io.servicetalk.concurrent.api.Publisher;
import io.servicetalk.http.api.DefaultHttpLoadBalancerFactory;
import io.servicetalk.http.api.DefaultStreamingHttpRequestResponseFactory;
import io.servicetalk.http.api.DelegatingHttpExecutionContext;
import io.servicetalk.http.api.FilterableStreamingHttpClient;
import io.servicetalk.http.api.FilterableStreamingHttpConnection;
import io.servicetalk.http.api.FilterableStreamingHttpLoadBalancedConnection;
import io.servicetalk.http.api.HttpExecutionContext;
import io.servicetalk.http.api.HttpExecutionStrategy;
import io.servicetalk.http.api.HttpHeadersFactory;
import io.servicetalk.http.api.HttpLoadBalancerFactory;
import io.servicetalk.http.api.HttpProtocolConfig;
import io.servicetalk.http.api.HttpProtocolVersion;
import io.servicetalk.http.api.ProxyConfig;
import io.servicetalk.http.api.SingleAddressHttpClientBuilder;
import io.servicetalk.http.api.StreamingHttpClient;
import io.servicetalk.http.api.StreamingHttpClientFilter;
import io.servicetalk.http.api.StreamingHttpClientFilterFactory;
import io.servicetalk.http.api.StreamingHttpConnectionFilterFactory;
import io.servicetalk.http.api.StreamingHttpRequest;
import io.servicetalk.http.api.StreamingHttpRequestResponseFactory;
import io.servicetalk.http.netty.ReservableRequestConcurrencyControllers.InternalRetryingHttpClientFilter;
import io.servicetalk.http.utils.HostHeaderHttpRequesterFilter;
import io.servicetalk.http.utils.IdleTimeoutConnectionFilter;
import io.servicetalk.loadbalancer.RoundRobinLoadBalancers;
import io.servicetalk.logging.api.LogLevel;
import io.servicetalk.transport.api.ClientSslConfig;
import io.servicetalk.transport.api.ExecutionStrategy;
import io.servicetalk.transport.api.HostAndPort;
import io.servicetalk.transport.api.IoExecutor;
import io.servicetalk.transport.api.TransportConfig;
import io.netty.handler.ssl.SslContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
import java.net.SocketOption;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.annotation.Nullable;
import static io.netty.util.NetUtil.toSocketAddressString;
import static io.servicetalk.client.api.ServiceDiscovererEvent.Status.UNAVAILABLE;
import static io.servicetalk.concurrent.api.AsyncCloseables.newCompositeCloseable;
import static io.servicetalk.concurrent.api.Processors.newCompletableProcessor;
import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy;
import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone;
import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1;
import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_2_0;
import static io.servicetalk.http.netty.AlpnIds.HTTP_2;
import static io.servicetalk.http.netty.StrategyInfluencerAwareConversions.toConditionalClientFilterFactory;
import static io.servicetalk.http.netty.StrategyInfluencerAwareConversions.toConditionalConnectionFilterFactory;
import static java.lang.Integer.parseInt;
import static java.time.Duration.ofMinutes;
import static java.util.Objects.requireNonNull;
/**
* A builder of {@link StreamingHttpClient} instances which call a single server based on the provided address.
*
* It also provides a good set of default settings and configurations, which could be used by most users as-is or
* could be overridden to address specific use cases.
*
* @param the type of address before resolution (unresolved address)
* @param the type of address after resolution (resolved address)
*/
final class DefaultSingleAddressHttpClientBuilder implements SingleAddressHttpClientBuilder {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSingleAddressHttpClientBuilder.class);
private static final RetryingHttpRequesterFilter DEFAULT_AUTO_RETRIES =
new RetryingHttpRequesterFilter.Builder().build();
private static final StreamingHttpConnectionFilterFactory DEFAULT_IDLE_TIMEOUT_FILTER =
new IdleTimeoutConnectionFilter(ofMinutes(5));
private static final AtomicInteger CLIENT_ID = new AtomicInteger();
private final U address;
@Nullable
private U proxyAddress;
private final HttpClientConfig config;
final HttpExecutionContextBuilder executionContextBuilder;
private final ClientStrategyInfluencerChainBuilder strategyComputation;
private HttpLoadBalancerFactory loadBalancerFactory;
private ServiceDiscoverer> serviceDiscoverer;
private Function hostToCharSequenceFunction =
DefaultSingleAddressHttpClientBuilder::toAuthorityForm;
private boolean addHostHeaderFallbackFilter = true;
private boolean addIdleTimeoutConnectionFilter = true;
@Nullable
private BiIntFunction serviceDiscovererRetryStrategy;
@Nullable
private StreamingHttpConnectionFilterFactory connectionFilterFactory;
@Nullable
private ContextAwareStreamingHttpClientFilterFactory clientFilterFactory;
private ConnectionFactoryFilter connectionFactoryFilter =
ConnectionFactoryFilter.identity();
@Nullable
private RetryingHttpRequesterFilter retryingHttpRequesterFilter;
// Do not use this ctor directly, HttpClients is the entry point for creating a new builder.
DefaultSingleAddressHttpClientBuilder(
final U address, final ServiceDiscoverer> serviceDiscoverer) {
this.address = requireNonNull(address);
config = new HttpClientConfig();
executionContextBuilder = new HttpExecutionContextBuilder();
strategyComputation = new ClientStrategyInfluencerChainBuilder();
this.loadBalancerFactory = defaultLoadBalancer();
this.serviceDiscoverer = new CastedServiceDiscoverer<>(serviceDiscoverer);
clientFilterFactory = appendFilter(clientFilterFactory, HttpMessageDiscardWatchdogClientFilter.CLIENT_CLEANER);
}
private DefaultSingleAddressHttpClientBuilder(final U address,
final DefaultSingleAddressHttpClientBuilder from) {
this.address = requireNonNull(address);
proxyAddress = from.proxyAddress;
config = new HttpClientConfig(from.config);
executionContextBuilder = new HttpExecutionContextBuilder(from.executionContextBuilder);
strategyComputation = from.strategyComputation.copy();
loadBalancerFactory = from.loadBalancerFactory;
serviceDiscoverer = from.serviceDiscoverer;
serviceDiscovererRetryStrategy = from.serviceDiscovererRetryStrategy;
clientFilterFactory = from.clientFilterFactory;
connectionFilterFactory = from.connectionFilterFactory;
hostToCharSequenceFunction = from.hostToCharSequenceFunction;
addHostHeaderFallbackFilter = from.addHostHeaderFallbackFilter;
addIdleTimeoutConnectionFilter = from.addIdleTimeoutConnectionFilter;
connectionFactoryFilter = from.connectionFactoryFilter;
retryingHttpRequesterFilter = from.retryingHttpRequesterFilter;
}
static SingleAddressHttpClientBuilder setExecutionContext(
final SingleAddressHttpClientBuilder builder, final HttpExecutionContext context) {
return builder.ioExecutor(context.ioExecutor())
.executor(context.executor())
.bufferAllocator(context.bufferAllocator())
.executionStrategy(context.executionStrategy());
}
private static final class HttpClientBuildContext {
final DefaultSingleAddressHttpClientBuilder builder;
private final ServiceDiscoverer> sd;
private final SdStatusCompletable sdStatus;
@Nullable
private final BiIntFunction serviceDiscovererRetryStrategy;
HttpClientBuildContext(
final DefaultSingleAddressHttpClientBuilder builder,
final ServiceDiscoverer> sd,
@Nullable final BiIntFunction serviceDiscovererRetryStrategy) {
this.builder = builder;
this.serviceDiscovererRetryStrategy = serviceDiscovererRetryStrategy;
this.sd = sd;
this.sdStatus = new SdStatusCompletable();
}
U address() {
return builder.proxyAddress != null ? builder.proxyAddress : builder.address;
}
HttpClientConfig httpConfig() {
return builder.config;
}
ServiceDiscoverer> serviceDiscoverer(
final String targetResource, final HttpExecutionContext executionContext) {
final BiIntFunction sdRetryStrategy = serviceDiscovererRetryStrategy;
if (sdRetryStrategy == HttpClients.NoRetriesStrategy.INSTANCE) {
return sd;
}
return new RetryingServiceDiscoverer<>(targetResource, new StatusAwareServiceDiscoverer<>(sd, sdStatus),
sdRetryStrategy, executionContext, HttpClientBuildContext::makeUnavailable);
}
private static ServiceDiscovererEvent makeUnavailable(final ServiceDiscovererEvent event) {
return new DefaultServiceDiscovererEvent<>(event.address(), UNAVAILABLE);
}
}
@Override
public StreamingHttpClient buildStreaming() {
return buildStreaming(copyBuildCtx());
}
private static StreamingHttpClient buildStreaming(final HttpClientBuildContext ctx) {
final String targetResource = targetResource(ctx);
final ReadOnlyHttpClientConfig roConfig = ctx.httpConfig().asReadOnly();
final HttpExecutionContext builderExecutionContext = ctx.builder.executionContextBuilder.build();
final HttpExecutionStrategy computedStrategy =
ctx.builder.strategyComputation.buildForClient(builderExecutionContext.executionStrategy());
final HttpExecutionContext executionContext = new DelegatingHttpExecutionContext(builderExecutionContext) {
@Override
public HttpExecutionStrategy executionStrategy() {
return computedStrategy;
}
};
final SslContext sslContext = roConfig.tcpConfig().sslContext();
// Track resources that potentially need to be closed when an exception is thrown during buildStreaming
final CompositeCloseable closeOnException = newCompositeCloseable();
try {
final Publisher>> sdEvents =
ctx.serviceDiscoverer(targetResource, executionContext).discover(ctx.address());
ConnectionFactoryFilter connectionFactoryFilter =
ctx.builder.connectionFactoryFilter;
final ExecutionStrategy connectionFactoryStrategy =
ctx.builder.strategyComputation.buildForConnectionFactory();
if (roConfig.hasProxy() && sslContext != null) {
assert roConfig.proxyConfig() != null;
@SuppressWarnings("deprecation")
final ConnectionFactoryFilter proxy =
new ProxyConnectConnectionFactoryFilter<>(roConfig.proxyConfig().address());
assert !proxy.requiredOffloads().hasOffloads();
connectionFactoryFilter = appendConnectionFilter(proxy, connectionFactoryFilter);
}
// Add HTTP request tracking. Extracting the RequestTracker from the context is done on the return
// path once we have a connection so that we want the HttpRequestTracker filter to prepended so that it is
// _last_ to see the newly created connection and try and extract the RequestTracker from the context.
connectionFactoryFilter = appendConnectionFilter(HttpRequestTracker.filter(), connectionFactoryFilter);
final HttpExecutionStrategy builderStrategy = executionContext.executionStrategy();
// closed by the LoadBalancer
final ConnectionFactory connectionFactory;
final StreamingHttpRequestResponseFactory reqRespFactory = defaultReqRespFactory(roConfig,
executionContext.bufferAllocator());
StreamingHttpConnectionFilterFactory connectionFilterFactory =
ctx.builder.addIdleTimeoutConnectionFilter ?
appendConnectionFilter(ctx.builder.connectionFilterFactory, DEFAULT_IDLE_TIMEOUT_FILTER) :
ctx.builder.connectionFilterFactory;
connectionFilterFactory = appendConnectionFilter(connectionFilterFactory,
HttpMessageDiscardWatchdogClientFilter.INSTANCE);
if (roConfig.isH2PriorKnowledge() &&
// Direct connection or HTTP proxy
(!roConfig.hasProxy() || sslContext == null)) {
H2ProtocolConfig h2Config = roConfig.h2Config();
assert h2Config != null;
connectionFactory = new H2LBHttpConnectionFactory<>(roConfig, executionContext,
connectionFilterFactory, reqRespFactory,
connectionFactoryStrategy, connectionFactoryFilter,
ctx.builder.loadBalancerFactory::toLoadBalancedConnection);
} else if (!roConfig.hasProxy() && roConfig.tcpConfig().preferredAlpnProtocol() != null) {
H1ProtocolConfig h1Config = roConfig.h1Config();
H2ProtocolConfig h2Config = roConfig.h2Config();
connectionFactory = new AlpnLBHttpConnectionFactory<>(roConfig, executionContext,
connectionFilterFactory, new AlpnReqRespFactoryFunc(
executionContext.bufferAllocator(),
h1Config == null ? null : h1Config.headersFactory(),
h2Config == null ? null : h2Config.headersFactory()),
connectionFactoryStrategy, connectionFactoryFilter,
ctx.builder.loadBalancerFactory::toLoadBalancedConnection);
} else if (roConfig.hasProxy() && sslContext != null) {
connectionFactory = new ProxyConnectLBHttpConnectionFactory<>(roConfig, executionContext,
connectionFilterFactory, reqRespFactory,
connectionFactoryStrategy, connectionFactoryFilter,
ctx.builder.loadBalancerFactory::toLoadBalancedConnection);
} else {
connectionFactory = new PipelinedLBHttpConnectionFactory<>(roConfig, executionContext,
connectionFilterFactory, reqRespFactory,
connectionFactoryStrategy, connectionFactoryFilter,
ctx.builder.loadBalancerFactory::toLoadBalancedConnection);
}
final LoadBalancer lb =
closeOnException.prepend(ctx.builder.loadBalancerFactory.newLoadBalancer(
sdEvents, connectionFactory, targetResource));
ContextAwareStreamingHttpClientFilterFactory currClientFilterFactory = ctx.builder.clientFilterFactory;
if (roConfig.hasProxy() && sslContext == null) {
// If we're talking to a proxy over http (not https), rewrite the request-target to absolute-form, as
// specified by the RFC: https://tools.ietf.org/html/rfc7230#section-5.3.2
currClientFilterFactory = appendFilter(currClientFilterFactory,
ctx.builder.proxyAbsoluteAddressFilterFactory());
}
if (ctx.builder.addHostHeaderFallbackFilter) {
currClientFilterFactory = appendFilter(currClientFilterFactory, new HostHeaderHttpRequesterFilter(
ctx.builder.hostToCharSequenceFunction.apply(ctx.builder.address)));
}
FilterableStreamingHttpClient lbClient = closeOnException.prepend(
new LoadBalancedStreamingHttpClient(executionContext, lb, reqRespFactory));
if (ctx.builder.retryingHttpRequesterFilter == null) {
ctx.builder.retryingHttpRequesterFilter = DEFAULT_AUTO_RETRIES;
currClientFilterFactory = appendFilter(currClientFilterFactory,
ctx.builder.retryingHttpRequesterFilter);
}
// Internal retries must be one of the last filters in the chain.
currClientFilterFactory = appendFilter(currClientFilterFactory, InternalRetryingHttpClientFilter.INSTANCE);
FilterableStreamingHttpClient wrappedClient =
currClientFilterFactory.create(lbClient, lb.eventStream(), ctx.sdStatus);
if (builderStrategy != defaultStrategy() &&
builderStrategy.missing(computedStrategy) != offloadNone()) {
LOGGER.info("Client for {} created with the builder strategy {} but resulting computed strategy is " +
"{}. One of the filters enforces additional offloading. To find out what filter is " +
"it, enable debug level logging for {}.", targetResource, builderStrategy,
computedStrategy, ClientStrategyInfluencerChainBuilder.class);
} else if (builderStrategy == computedStrategy) {
LOGGER.debug("Client for {} created with the execution strategy {}.",
targetResource, computedStrategy);
} else {
LOGGER.debug("Client for {} created with the builder strategy {}, resulting computed strategy is {}.",
targetResource, builderStrategy, computedStrategy);
}
return new FilterableClientToClient(wrappedClient, executionContext);
} catch (final Throwable t) {
closeOnException.closeAsync().subscribe();
throw t;
}
}
private static ConnectionFactoryFilter appendConnectionFilter(
final ConnectionFactoryFilter first,
final ConnectionFactoryFilter second) {
// For now this delegates to the deprecated method but needs to be fixed up once the deprecated append
// method is being removed.
return first.append(second);
}
private static StreamingHttpRequestResponseFactory defaultReqRespFactory(ReadOnlyHttpClientConfig roConfig,
BufferAllocator allocator) {
if (roConfig.isH2PriorKnowledge()) {
H2ProtocolConfig h2Config = roConfig.h2Config();
assert h2Config != null;
return new DefaultStreamingHttpRequestResponseFactory(allocator, h2Config.headersFactory(), HTTP_2_0);
} else if (roConfig.tcpConfig().preferredAlpnProtocol() != null) {
H1ProtocolConfig h1Config = roConfig.h1Config();
H2ProtocolConfig h2Config = roConfig.h2Config();
// The client can't know which protocol will be negotiated on the connection/transport level, so just
// default to the preferred protocol and pay additional conversion costs if we don't get our preference.
String preferredAlpnProtocol = roConfig.tcpConfig().preferredAlpnProtocol();
if (HTTP_2.equals(preferredAlpnProtocol)) {
assert h2Config != null;
return new DefaultStreamingHttpRequestResponseFactory(allocator, h2Config.headersFactory(), HTTP_2_0);
} else { // just fall back to h1, all other protocols should convert to/from it.
if (h1Config == null) {
throw new IllegalStateException(preferredAlpnProtocol +
" is preferred ALPN protocol, falling back to " + HTTP_1_1 + " but the " + HTTP_1_1 +
" protocol was not configured.");
}
return new DefaultStreamingHttpRequestResponseFactory(allocator, h1Config.headersFactory(), HTTP_1_1);
}
} else {
H1ProtocolConfig h1Config = roConfig.h1Config();
assert h1Config != null;
return new DefaultStreamingHttpRequestResponseFactory(allocator, h1Config.headersFactory(), HTTP_1_1);
}
}
/**
* This method is used to create a "targetResource" identifier that helps us to correlate internal state of the
* ServiceDiscoveryRetryStrategy and LoadBalancer.
*/
private static String targetResource(final HttpClientBuildContext ctx) {
final String uniqueAddress = ctx.builder.address + "/" + CLIENT_ID.incrementAndGet();
return ctx.builder.proxyAddress == null ? uniqueAddress :
uniqueAddress + " (via " + ctx.builder.proxyAddress + ")";
}
private static ContextAwareStreamingHttpClientFilterFactory appendFilter(
@Nullable final ContextAwareStreamingHttpClientFilterFactory currClientFilterFactory,
final StreamingHttpClientFilterFactory appendClientFilterFactory) {
if (appendClientFilterFactory instanceof RetryingHttpRequesterFilter) {
if (currClientFilterFactory == null) {
return (client, lbEventStream, sdStatus) -> {
final RetryingHttpRequesterFilter.ContextAwareRetryingHttpClientFilter filter =
(RetryingHttpRequesterFilter.ContextAwareRetryingHttpClientFilter)
appendClientFilterFactory.create(client);
filter.inject(lbEventStream, sdStatus);
return filter;
};
} else {
return (client, lbEventStream, sdStatus) -> {
final RetryingHttpRequesterFilter.ContextAwareRetryingHttpClientFilter filter =
(RetryingHttpRequesterFilter.ContextAwareRetryingHttpClientFilter)
appendClientFilterFactory.create(client);
filter.inject(lbEventStream, sdStatus);
return currClientFilterFactory.create(filter, lbEventStream, sdStatus);
};
}
} else if (appendClientFilterFactory instanceof ContextAwareStreamingHttpClientFilterFactory) {
if (currClientFilterFactory == null) {
return ((ContextAwareStreamingHttpClientFilterFactory) appendClientFilterFactory);
} else {
return (client, lbEventStream, sdError) ->
currClientFilterFactory.create(
((ContextAwareStreamingHttpClientFilterFactory) appendClientFilterFactory)
.create(client, lbEventStream, sdError), lbEventStream, sdError);
}
} else {
if (currClientFilterFactory == null) {
return (client, lbEventStream, sdError) -> appendClientFilterFactory.create(client);
} else {
return (client, lbEventStream, sdError) ->
currClientFilterFactory.create(appendClientFilterFactory.create(client),
lbEventStream, sdError);
}
}
}
/**
* Creates a context before building the client, avoid concurrent changes at runtime.
*/
private HttpClientBuildContext copyBuildCtx() {
return new HttpClientBuildContext<>(new DefaultSingleAddressHttpClientBuilder<>(address, this),
this.serviceDiscoverer, this.serviceDiscovererRetryStrategy);
}
private AbsoluteAddressHttpRequesterFilter proxyAbsoluteAddressFilterFactory() {
return new AbsoluteAddressHttpRequesterFilter("http", hostToCharSequenceFunction.apply(address));
}
@Override
public DefaultSingleAddressHttpClientBuilder proxyConfig(final ProxyConfig proxyConfig) {
this.proxyAddress = requireNonNull(proxyConfig.address());
config.proxyConfig(hostToCharSequenceFunction.apply(address), proxyConfig);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder ioExecutor(final IoExecutor ioExecutor) {
executionContextBuilder.ioExecutor(ioExecutor);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder executor(final Executor executor) {
executionContextBuilder.executor(executor);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder executionStrategy(final HttpExecutionStrategy strategy) {
executionContextBuilder.executionStrategy(strategy);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder bufferAllocator(final BufferAllocator allocator) {
executionContextBuilder.bufferAllocator(allocator);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder socketOption(SocketOption option, T value) {
config.tcpConfig().socketOption(option, value);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder enableWireLogging(final String loggerName,
final LogLevel logLevel,
final BooleanSupplier logUserData) {
config.tcpConfig().enableWireLogging(loggerName, logLevel, logUserData);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder protocols(final HttpProtocolConfig... protocols) {
config.protocolConfigs().protocols(protocols);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder appendConnectionFilter(
final StreamingHttpConnectionFilterFactory factory) {
requireNonNull(factory);
connectionFilterFactory = appendConnectionFilter(connectionFilterFactory, factory);
strategyComputation.add(factory);
checkIfHostHeaderHttpRequesterFilter(factory);
checkIfIdleTimeoutConnectionFilter(factory);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder appendConnectionFilter(
final Predicate predicate, final StreamingHttpConnectionFilterFactory factory) {
appendConnectionFilter(toConditionalConnectionFilterFactory(predicate, factory));
checkIfHostHeaderHttpRequesterFilter(factory);
checkIfIdleTimeoutConnectionFilter(factory);
return this;
}
private void checkIfHostHeaderHttpRequesterFilter(final Object filter) {
if (filter instanceof HostHeaderHttpRequesterFilter) {
addHostHeaderFallbackFilter = false;
}
}
private void checkIfIdleTimeoutConnectionFilter(final StreamingHttpConnectionFilterFactory factory) {
if (factory instanceof IdleTimeoutConnectionFilter) {
addIdleTimeoutConnectionFilter = false;
}
}
// Use another method to keep final references and avoid StackOverflowError
private static StreamingHttpConnectionFilterFactory appendConnectionFilter(
@Nullable final StreamingHttpConnectionFilterFactory current,
final StreamingHttpConnectionFilterFactory next) {
return current == null ? next : connection -> current.create(next.create(connection));
}
@Override
public DefaultSingleAddressHttpClientBuilder appendConnectionFactoryFilter(
final ConnectionFactoryFilter factory) {
connectionFactoryFilter = appendConnectionFilter(connectionFactoryFilter, requireNonNull(factory));
strategyComputation.add(factory);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder hostHeaderFallback(final boolean enable) {
addHostHeaderFallbackFilter = enable;
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder allowDropResponseTrailers(final boolean allowDrop) {
config.protocolConfigs().allowDropTrailersReadFromTransport(allowDrop);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder appendClientFilter(
final Predicate predicate, final StreamingHttpClientFilterFactory factory) {
checkIfRetryingHttpRequesterFilter(factory);
appendClientFilter(toConditionalClientFilterFactory(predicate, factory));
checkIfHostHeaderHttpRequesterFilter(factory);
return this;
}
private void checkIfRetryingHttpRequesterFilter(final StreamingHttpClientFilterFactory factory) {
if (factory instanceof RetryingHttpRequesterFilter) {
if (retryingHttpRequesterFilter != null) {
throw new IllegalStateException("Retrying HTTP requester filter was already found in " +
"the filter chain, only a single instance of that is allowed.");
}
retryingHttpRequesterFilter = (RetryingHttpRequesterFilter) factory;
}
}
@Override
public DefaultSingleAddressHttpClientBuilder unresolvedAddressToHost(
final Function unresolvedAddressToHostFunction) {
this.hostToCharSequenceFunction = requireNonNull(unresolvedAddressToHostFunction);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder appendClientFilter(
final StreamingHttpClientFilterFactory factory) {
requireNonNull(factory);
checkIfRetryingHttpRequesterFilter(factory);
clientFilterFactory = appendFilter(clientFilterFactory, factory);
strategyComputation.add(factory);
checkIfHostHeaderHttpRequesterFilter(factory);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder serviceDiscoverer(
final ServiceDiscoverer> serviceDiscoverer) {
this.serviceDiscoverer = new CastedServiceDiscoverer<>(serviceDiscoverer);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder retryServiceDiscoveryErrors(
final BiIntFunction retryStrategy) {
this.serviceDiscovererRetryStrategy = requireNonNull(retryStrategy);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder loadBalancerFactory(
final HttpLoadBalancerFactory loadBalancerFactory) {
this.loadBalancerFactory = requireNonNull(loadBalancerFactory);
strategyComputation.add(loadBalancerFactory);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder sslConfig(ClientSslConfig sslConfig) {
// defer setting the fallback host/port so the user has a chance to configure hostToCharSequenceFunction.
setFallbackHostAndPort(config, address);
config.tcpConfig().sslConfig(sslConfig);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder inferPeerHost(boolean shouldInfer) {
config.inferPeerHost(shouldInfer);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder inferPeerPort(boolean shouldInfer) {
config.inferPeerPort(shouldInfer);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder inferSniHostname(boolean shouldInfer) {
config.inferSniHostname(shouldInfer);
return this;
}
@Override
public DefaultSingleAddressHttpClientBuilder transportConfig(final TransportConfig transportConfig) {
config.tcpConfig().transportConfig(transportConfig);
return this;
}
private static CharSequence toAuthorityForm(final U address) {
if (address instanceof CharSequence) {
return (CharSequence) address;
}
if (address instanceof HostAndPort) {
final HostAndPort hostAndPort = (HostAndPort) address;
return toSocketAddressString(hostAndPort.hostName(), hostAndPort.port());
}
if (address instanceof InetSocketAddress) {
return toSocketAddressString((InetSocketAddress) address);
}
return address.toString();
}
private void setFallbackHostAndPort(HttpClientConfig config, U address) {
if (address instanceof HostAndPort) {
HostAndPort hostAndPort = (HostAndPort) address;
config.fallbackPeerHost(hostAndPort.hostName());
config.fallbackPeerPort(hostAndPort.port());
} else if (address instanceof InetSocketAddress) {
InetSocketAddress inetSocketAddress = (InetSocketAddress) address;
config.fallbackPeerHost(inetSocketAddress.getHostString());
config.fallbackPeerPort(inetSocketAddress.getPort());
} else {
CharSequence cs = hostToCharSequenceFunction.apply(address);
if (cs == null) {
config.fallbackPeerHost(null);
config.fallbackPeerPort(-1);
} else {
int colon = CharSequences.indexOf(cs, ':', 0);
if (colon < 0) {
config.fallbackPeerHost(cs.toString());
config.fallbackPeerPort(-1);
} else if (cs.charAt(0) == '[') {
colon = CharSequences.indexOf(cs, ']', 1);
if (colon < 0) {
throw new IllegalArgumentException("unable to find end ']' of IPv6 address: " + cs);
}
config.fallbackPeerHost(cs.subSequence(1, colon).toString());
++colon;
if (colon >= cs.length()) {
config.fallbackPeerPort(-1);
} else if (cs.charAt(colon) != ':') {
throw new IllegalArgumentException("':' expected after ']' for IPv6 address: " + cs);
} else {
config.fallbackPeerPort(parseInt(cs.subSequence(colon + 1, cs.length()).toString()));
}
} else {
config.fallbackPeerHost(cs.subSequence(0, colon).toString());
config.fallbackPeerPort(parseInt(cs.subSequence(colon + 1, cs.length()).toString()));
}
}
}
}
private static final class SdStatusCompletable extends Completable {
private volatile CompletableSource.Processor processor = newCompletableProcessor();
private boolean seenError; // this is only accessed from nextError and resetError which are not concurrent
@Override
protected void handleSubscribe(final Subscriber subscriber) {
processor.subscribe(subscriber);
}
void nextError(final Throwable t) {
seenError = true;
// This state is reused across multiple subscribes, and we reset the processor to deliver the latest error
// to new subscribers.
final CompletableSource.Processor oldProcessor = processor;
oldProcessor.onError(t);
final CompletableSource.Processor newProcessor = newCompletableProcessor();
newProcessor.onError(t);
processor = newProcessor;
}
void resetError() {
if (seenError) {
processor = newCompletableProcessor();
seenError = false;
}
}
}
private static final class StatusAwareServiceDiscoverer>
extends DelegatingServiceDiscoverer {
private static final Logger LOGGER = LoggerFactory.getLogger(StatusAwareServiceDiscoverer.class);
private final SdStatusCompletable status;
StatusAwareServiceDiscoverer(final ServiceDiscoverer delegate, final SdStatusCompletable status) {
super(delegate);
this.status = status;
}
@Override
public Publisher> discover(final U u) {
return delegate().discover(u)
.beforeOnError(t -> {
LOGGER.debug("Observed an error from {} while discovering '{}':", delegate(), u, t);
status.nextError(t);
})
.beforeOnNext(__ -> status.resetError());
// We do not complete sdStatus to let LB decide when to retry if SD completes.
}
}
private static final class AlpnReqRespFactoryFunc implements
Function {
private final BufferAllocator allocator;
@Nullable
private final HttpHeadersFactory h1HeadersFactory;
@Nullable
private final HttpHeadersFactory h2HeadersFactory;
@Nullable
private StreamingHttpRequestResponseFactory h1Factory;
@Nullable
private StreamingHttpRequestResponseFactory h2Factory;
AlpnReqRespFactoryFunc(final BufferAllocator allocator, @Nullable final HttpHeadersFactory h1HeadersFactory,
@Nullable final HttpHeadersFactory h2HeadersFactory) {
this.allocator = allocator;
this.h1HeadersFactory = h1HeadersFactory;
this.h2HeadersFactory = h2HeadersFactory;
}
@Override
public StreamingHttpRequestResponseFactory apply(final HttpProtocolVersion version) {
if (version == HTTP_1_1) {
if (h1Factory == null) { // it is OK if we race and re-initialize the instance.
h1Factory = new DefaultStreamingHttpRequestResponseFactory(allocator,
headersFactory(h1HeadersFactory, HTTP_1_1), HTTP_1_1);
}
return h1Factory;
} else if (version == HTTP_2_0) {
if (h2Factory == null) { // it is OK if we race and re-initialize the instance.
h2Factory = new DefaultStreamingHttpRequestResponseFactory(allocator,
headersFactory(h2HeadersFactory, HTTP_2_0), HTTP_2_0);
}
return h2Factory;
} else if (version.major() <= 1) {
// client doesn't generate HTTP_1_0 requests, no need to cache.
return new DefaultStreamingHttpRequestResponseFactory(allocator,
headersFactory(h1HeadersFactory, version), version);
} else if (version.major() == 2) {
return new DefaultStreamingHttpRequestResponseFactory(allocator,
headersFactory(h2HeadersFactory, version), version);
} else {
throw new IllegalArgumentException("unsupported protocol: " + version);
}
}
private static HttpHeadersFactory headersFactory(@Nullable HttpHeadersFactory factory,
HttpProtocolVersion version) {
if (factory == null) {
throw new IllegalStateException("HeadersFactory config not found for selected protocol: " + version);
}
return factory;
}
}
@FunctionalInterface
interface ContextAwareStreamingHttpClientFilterFactory extends StreamingHttpClientFilterFactory {
StreamingHttpClientFilter create(FilterableStreamingHttpClient client,
@Nullable Publisher