
com.hotels.styx.client.StyxHttpClient Maven / Gradle / Ivy
/**
* Copyright (C) 2013-2018 Expedia 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.hotels.styx.client;
import com.google.common.collect.ImmutableList;
import com.hotels.styx.api.HttpClient;
import com.hotels.styx.api.HttpRequest;
import com.hotels.styx.api.HttpResponse;
import com.hotels.styx.api.Id;
import com.hotels.styx.api.client.Origin;
import com.hotels.styx.api.client.RemoteHost;
import com.hotels.styx.api.client.loadbalancing.spi.LoadBalancingStrategy;
import com.hotels.styx.api.client.retrypolicy.spi.RetryPolicy;
import com.hotels.styx.api.metrics.MetricRegistry;
import com.hotels.styx.api.metrics.codahale.CodaHaleMetricRegistry;
import com.hotels.styx.api.netty.exceptions.NoAvailableHostsException;
import com.hotels.styx.client.applications.BackendService;
import com.hotels.styx.client.applications.OriginStats;
import com.hotels.styx.client.retry.RetryNTimes;
import com.hotels.styx.client.stickysession.StickySessionLoadBalancingStrategy;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.slf4j.Logger;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static com.google.common.base.Objects.toStringHelper;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.getFirst;
import static com.hotels.styx.api.HttpHeaderNames.CONTENT_LENGTH;
import static com.hotels.styx.api.HttpHeaderNames.TRANSFER_ENCODING;
import static com.hotels.styx.client.stickysession.StickySessionCookie.newStickySessionCookie;
import static io.netty.handler.codec.http.HttpMethod.HEAD;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static java.util.stream.StreamSupport.stream;
import static org.slf4j.LoggerFactory.getLogger;
/**
* A configurable HTTP client that uses connection pooling, load balancing, etc.
*/
public final class StyxHttpClient implements HttpClient {
private static final Logger LOGGER = getLogger(StyxHttpClient.class);
private static final int MAX_RETRY_ATTEMPTS = 3;
private final Id id;
private final RewriteRuleset rewriteRuleset;
private final LoadBalancingStrategy loadBalancingStrategy;
private final RetryPolicy retryPolicy;
private final OriginStatsFactory originStatsFactory;
private final BackendService backendService;
private final MetricRegistry metricsRegistry;
private final boolean contentValidation;
private StyxHttpClient(Builder builder) {
this.backendService = requireNonNull(builder.backendService);
this.id = backendService.id();
this.originStatsFactory = requireNonNull(builder.originStatsFactory);
this.loadBalancingStrategy = requireNonNull(builder.loadBalancingStrategy);
this.retryPolicy = builder.retryPolicy != null
? builder.retryPolicy
: new RetryNTimes(3);
this.rewriteRuleset = new RewriteRuleset(builder.rewriteRules);
this.metricsRegistry = builder.metricsRegistry;
this.contentValidation = builder.contentValidation;
}
@Override
public Observable sendRequest(HttpRequest request) {
return sendRequest(rewriteUrl(request), new ArrayList<>(), 0);
}
public boolean isHttps() {
return backendService.tlsSettings().isPresent();
}
/**
* Create a new builder.
*
* @return a new builder
*/
public static Builder newHttpClientBuilder(BackendService backendService) {
return new Builder(backendService);
}
private static boolean isError(HttpResponseStatus status) {
return status.code() >= 400;
}
private static boolean bodyNeedsToBeRemoved(HttpRequest request, HttpResponse response) {
return isHeadRequest(request) || isBodilessResponse(response);
}
private static HttpResponse responseWithoutBody(HttpResponse response) {
return response.newBuilder()
.header(CONTENT_LENGTH, 0)
.removeHeader(TRANSFER_ENCODING)
.removeBody()
.build();
}
private static boolean isBodilessResponse(HttpResponse response) {
int status = response.status().code();
return status == 204 || status == 304 || status / 100 == 1;
}
private static boolean isHeadRequest(HttpRequest request) {
return request.method().equals(HEAD);
}
private Observable sendRequest(HttpRequest request, List previousOrigins, int attempt) {
if (attempt >= MAX_RETRY_ATTEMPTS) {
return Observable.error(new NoAvailableHostsException(this.id));
}
Optional remoteHost = selectOrigin(request);
if (remoteHost.isPresent()) {
RemoteHost host = remoteHost.get();
previousOrigins.add(host);
return host.hostClient()
.sendRequest(request)
.map(response -> addStickySessionIdentifier(response, host.origin()))
.doOnError(throwable -> logError(request, throwable))
.doOnUnsubscribe(() -> originStatsFactory.originStats(host.origin()).requestCancelled())
.doOnNext(this::recordErrorStatusMetrics)
.map(response -> removeUnexpectedResponseBody(request, response))
.map(this::removeRedundantContentLengthHeader)
.onErrorResumeNext(cause -> {
RetryPolicyContext retryContext = new RetryPolicyContext(this.id, attempt + 1, cause, request, previousOrigins);
return retry(request, retryContext, previousOrigins, attempt + 1, cause);
});
} else {
RetryPolicyContext retryContext = new RetryPolicyContext(this.id, attempt + 1, null, request, previousOrigins);
return retry(request, retryContext, previousOrigins, attempt + 1, new NoAvailableHostsException(this.id));
}
}
Observable retry(HttpRequest request, RetryPolicyContext retryContext, List previousOrigins, int attempt, Throwable cause) {
LoadBalancingStrategy.Context lbContext = new LBContext(request, id, originStatsFactory);
if (this.retryPolicy.evaluate(retryContext, loadBalancingStrategy, lbContext).shouldRetry()) {
return sendRequest(request, previousOrigins, attempt);
} else {
return Observable.error(cause);
}
}
private static final class RetryPolicyContext implements RetryPolicy.Context {
private final Id appId;
private final int retryCount;
private final Throwable lastException;
private final HttpRequest request;
private final Iterable previouslyUsedOrigins;
RetryPolicyContext(Id appId, int retryCount, Throwable lastException, HttpRequest request,
Iterable previouslyUsedOrigins) {
this.appId = appId;
this.retryCount = retryCount;
this.lastException = lastException;
this.request = request;
this.previouslyUsedOrigins = previouslyUsedOrigins;
}
@Override
public Id appId() {
return appId;
}
@Override
public int currentRetryCount() {
return retryCount;
}
@Override
public Optional lastException() {
return Optional.ofNullable(lastException);
}
@Override
public HttpRequest currentRequest() {
return request;
}
@Override
public Iterable previousOrigins() {
return previouslyUsedOrigins;
}
@Override
public String toString() {
return toStringHelper(this)
.add("appId", appId)
.add("retryCount", retryCount)
.add("lastException", lastException)
.add("request", request.url())
.add("previouslyUsedOrigins", hosts(previouslyUsedOrigins))
.toString();
}
private static String hosts(Iterable origins) {
return stream(origins.spliterator(), false)
.map(host -> host.origin().hostAsString())
.collect(joining(", "));
}
}
static class LBContext implements LoadBalancingStrategy.Context {
private final HttpRequest request;
private final Id id;
private final OriginStatsFactory originStatsFactory;
LBContext(HttpRequest request, Id id, OriginStatsFactory originStatsFactory) {
this.request = requireNonNull(request);
this.id = requireNonNull(id);
this.originStatsFactory = requireNonNull(originStatsFactory);
}
@Override
public Id appId() {
return id;
}
@Override
public HttpRequest currentRequest() {
return request;
}
@Override
public double oneMinuteRateForStatusCode5xx(Origin origin) {
OriginStats originStats = originStatsFactory.originStats(origin);
return originStats.oneMinuteErrorRate();
}
}
private static void logError(HttpRequest rewrittenRequest, Throwable throwable) {
LOGGER.error("Error Handling request={} exceptionClass={} exceptionMessage=\"{}\"",
new Object[]{rewrittenRequest, throwable.getClass().getName(), throwable.getMessage()});
}
private HttpResponse removeUnexpectedResponseBody(HttpRequest request, HttpResponse response) {
if (contentValidation && bodyNeedsToBeRemoved(request, response)) {
return responseWithoutBody(response);
} else {
return response;
}
}
private HttpResponse removeRedundantContentLengthHeader(HttpResponse response) {
if (contentValidation && response.contentLength().isPresent() && response.chunked()) {
return response.newBuilder()
.removeHeader(CONTENT_LENGTH)
.build();
}
return response;
}
private void recordErrorStatusMetrics(HttpResponse response) {
if (isError(response.status())) {
metricsRegistry.counter("origins.response.status." + response.status().code()).inc();
}
}
private Optional selectOrigin(HttpRequest rewrittenRequest) {
LoadBalancingStrategy.Context lbContext = new LBContext(rewrittenRequest, id, originStatsFactory);
Iterable votedOrigins = loadBalancingStrategy.vote(lbContext);
return Optional.ofNullable(getFirst(votedOrigins, null));
}
private HttpResponse addStickySessionIdentifier(HttpResponse httpResponse, Origin origin) {
if (this.loadBalancingStrategy instanceof StickySessionLoadBalancingStrategy) {
int maxAge = backendService.stickySessionConfig().stickySessionTimeoutSeconds();
return httpResponse.newBuilder()
.addCookie(newStickySessionCookie(id, origin.id(), maxAge))
.build();
} else {
return httpResponse;
}
}
private HttpRequest rewriteUrl(HttpRequest request) {
return rewriteRuleset.rewrite(request);
}
@Override
public String toString() {
return toStringHelper(this)
.add("id", id)
.add("stickySessionConfig", backendService.stickySessionConfig())
.add("retryPolicy", retryPolicy)
.add("rewriteRuleset", rewriteRuleset)
.add("loadBalancingStrategy", loadBalancingStrategy)
.toString();
}
/**
* A builder for {@link com.hotels.styx.client.StyxHttpClient}.
*/
public static class Builder {
private final BackendService backendService;
private MetricRegistry metricsRegistry = new CodaHaleMetricRegistry();
private List rewriteRules = emptyList();
private RetryPolicy retryPolicy = new RetryNTimes(3);
private LoadBalancingStrategy loadBalancingStrategy;
private boolean contentValidation;
private OriginStatsFactory originStatsFactory;
public Builder(BackendService backendService) {
this.backendService = checkNotNull(backendService);
}
public Builder metricsRegistry(MetricRegistry metricsRegistry) {
this.metricsRegistry = checkNotNull(metricsRegistry);
return this;
}
public Builder retryPolicy(RetryPolicy retryPolicy) {
this.retryPolicy = checkNotNull(retryPolicy);
return this;
}
public Builder rewriteRules(List extends RewriteRule> rewriteRules) {
this.rewriteRules = ImmutableList.copyOf(rewriteRules);
return this;
}
public Builder loadBalancingStrategy(LoadBalancingStrategy loadBalancingStrategy) {
this.loadBalancingStrategy = requireNonNull(loadBalancingStrategy);
return this;
}
public Builder enableContentValidation() {
contentValidation = true;
return this;
}
public Builder originStatsFactory(OriginStatsFactory originStatsFactory) {
this.originStatsFactory = originStatsFactory;
return this;
}
public StyxHttpClient build() {
if (originStatsFactory == null) {
originStatsFactory = new OriginStatsFactory(metricsRegistry);
}
return new StyxHttpClient(this);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy