com.proofpoint.http.client.balancing.BalancingHttpClient Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2013 Proofpoint, 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.proofpoint.http.client.balancing;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ticker;
import com.google.common.cache.Cache;
import com.google.common.util.concurrent.AbstractFuture;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import com.proofpoint.http.client.HttpClient;
import com.proofpoint.http.client.Request;
import com.proofpoint.http.client.RequestStats;
import com.proofpoint.http.client.ResponseHandler;
import com.proofpoint.http.client.jetty.JettyHttpClient;
import com.proofpoint.tracetoken.TraceToken;
import com.proofpoint.tracetoken.TraceTokenScope;
import com.proofpoint.units.Duration;
import jakarta.inject.Inject;
import org.weakref.jmx.Flatten;
import org.weakref.jmx.Managed;
import java.net.URI;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.cache.CacheBuilder.newBuilder;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static com.proofpoint.tracetoken.TraceTokenManager.getCurrentTraceToken;
import static com.proofpoint.tracetoken.TraceTokenManager.registerTraceToken;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
public class BalancingHttpClient
implements HttpClient
{
private static final Duration ZERO_DURATION = new Duration(0, TimeUnit.MILLISECONDS);
private final HttpServiceBalancer pool;
private final HttpClient httpClient;
private final int maxAttempts;
private final RetryBudget retryBudget;
private final BackoffPolicy backoffPolicy;
private final ScheduledExecutorService retryExecutor;
private final Cache, Boolean> exceptionCache = newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();
@Inject
public BalancingHttpClient(@ForBalancingHttpClient HttpServiceBalancer pool,
@ForBalancingHttpClient HttpClient httpClient,
BalancingHttpClientConfig config,
@ForBalancingHttpClient ScheduledExecutorService retryExecutor)
{
this(pool, httpClient, config, retryExecutor, Ticker.systemTicker());
}
@VisibleForTesting
BalancingHttpClient(@ForBalancingHttpClient HttpServiceBalancer pool,
@ForBalancingHttpClient HttpClient httpClient,
BalancingHttpClientConfig config,
@ForBalancingHttpClient ScheduledExecutorService retryExecutor,
Ticker ticker)
{
this.pool = requireNonNull(pool, "pool is null");
this.httpClient = requireNonNull(httpClient, "httpClient is null");
maxAttempts = requireNonNull(config, "config is null").getMaxAttempts();
retryBudget = TokenRetryBudget.tokenRetryBudget(config.getRetryBudgetRatio(), config.getRetryBudgetRatioPeriod(), config.getRetryBudgetMinPerSecond(), ticker);
backoffPolicy = new DecorrelatedJitteredBackoffPolicy(config.getMinBackoff(), config.getMaxBackoff());
this.retryExecutor = requireNonNull(retryExecutor, "retryExecutor is null");
}
@Override
public T execute(Request request, ResponseHandler responseHandler)
throws E
{
checkArgument(!request.getUri().isAbsolute(), request.getUri() + " is not a relative URI");
checkArgument(request.getUri().getHost() == null, request.getUri() + " has a host component");
String path = request.getUri().getPath();
checkArgument(path == null || !path.startsWith("/"), request.getUri() + " path starts with '/'");
HttpServiceAttempt attempt;
try {
attempt = pool.createAttempt();
}
catch (RuntimeException e) {
return responseHandler.handleException(request, e);
}
int attemptsLeft = maxAttempts;
retryBudget.initialAttempt();
BackoffPolicy attemptBackoffPolicy = backoffPolicy;
Duration previousBackoff = ZERO_DURATION;
RetryingResponseHandler retryingResponseHandler = new RetryingResponseHandler<>(responseHandler, retryBudget, exceptionCache);
for (;;) {
URI uri = attempt.getUri();
if (!uri.toString().endsWith("/")) {
uri = URI.create(uri.toString() + '/');
}
uri = uri.resolve(request.getUri());
Request subRequest = Request.Builder.fromRequest(request)
.setUri(uri)
.build();
if (attemptsLeft <= 1) {
retryingResponseHandler = new RetryingResponseHandler<>(responseHandler, NoRetryBudget.INSTANCE, exceptionCache);
}
--attemptsLeft;
try {
T t = httpClient.execute(subRequest, retryingResponseHandler);
attempt.markGood();
return t;
}
catch (InnerHandlerException e) {
attempt.markBad(e.getFailureCategory(), e.getHandlerCategory());
//noinspection unchecked
throw (E) e.getCause();
}
catch (FailureStatusException e) {
attempt.markBad(e.getFailureCategory());
//noinspection unchecked
return (T) e.result;
}
catch (RetryException e) {
attempt.markBad(e.getFailureCategory());
Duration backoff = attemptBackoffPolicy.backoff(previousBackoff);
long millis = backoff.roundTo(MILLISECONDS);
try {
Thread.sleep(millis);
}
catch (InterruptedException e1) {
Thread.currentThread().interrupt();
return responseHandler.handleException(request, e1);
}
try {
attempt = attempt.next();
previousBackoff = backoff;
attemptBackoffPolicy = attemptBackoffPolicy.nextAttempt();
}
catch (RuntimeException e1) {
return responseHandler.handleException(request, e1);
}
}
}
}
@Override
public HttpResponseFuture executeAsync(Request request, ResponseHandler responseHandler)
{
checkArgument(!request.getUri().isAbsolute(), request.getUri() + " is not a relative URI");
checkArgument(request.getUri().getHost() == null, request.getUri() + " has a host component");
String path = request.getUri().getPath();
checkArgument(path == null || !path.startsWith("/"), request.getUri() + " path starts with '/'");
HttpServiceAttempt attempt;
try {
attempt = pool.createAttempt();
}
catch (RuntimeException e) {
try {
return new ImmediateHttpResponseFuture<>(responseHandler.handleException(request, e));
}
catch (Exception e1) {
return new ImmediateFailedHttpResponseFuture<>((E) e1);
}
}
retryBudget.initialAttempt();
RetryFuture retryFuture = new RetryFuture<>(request, responseHandler);
attemptQuery(retryFuture, request, responseHandler, attempt, maxAttempts);
return retryFuture;
}
private void attemptQuery(RetryFuture retryFuture, Request request, ResponseHandler responseHandler, HttpServiceAttempt attempt, int attemptsLeft)
{
RetryingResponseHandler retryingResponseHandler = new RetryingResponseHandler<>(
responseHandler,
(attemptsLeft <= 1) ? NoRetryBudget.INSTANCE : retryBudget,
exceptionCache
);
URI uri = attempt.getUri();
if (!uri.toString().endsWith("/")) {
uri = URI.create(uri.toString() + '/');
}
uri = uri.resolve(request.getUri());
Request subRequest = Request.Builder.fromRequest(request)
.setUri(uri)
.build();
--attemptsLeft;
HttpResponseFuture future = httpClient.executeAsync(subRequest, retryingResponseHandler);
retryFuture.newAttempt(future, attempt, uri, attemptsLeft);
}
@Flatten
@Override
public RequestStats getStats()
{
return httpClient.getStats();
}
@Flatten
RetryBudget getRetryBudget() {
return retryBudget;
}
@Managed
public String dump()
{
if (httpClient instanceof JettyHttpClient) {
return ((JettyHttpClient) httpClient).dump();
}
return null;
}
@Managed
public void dumpStdErr()
{
if (httpClient instanceof JettyHttpClient) {
((JettyHttpClient) httpClient).dumpStdErr();
}
}
@Override
public void close()
{
retryExecutor.shutdown();
retryExecutor.shutdownNow();
httpClient.close();
}
@Override
public boolean isClosed()
{
return httpClient.isClosed();
}
private class RetryFuture
extends AbstractFuture
implements HttpResponseFuture
{
private final Request request;
private final ResponseHandler responseHandler;
private final Object subFutureLock = new Object();
@GuardedBy("subFutureLock")
private HttpServiceAttempt attempt = null;
@GuardedBy("subFutureLock")
private BackoffPolicy attemptBackoffPolicy = backoffPolicy;
@GuardedBy("subFutureLock")
private Duration previousBackoff = ZERO_DURATION;
@GuardedBy("subFutureLock")
private URI uri = null;
@GuardedBy("subFutureLock")
private HttpResponseFuture subFuture = null;
RetryFuture(Request request, ResponseHandler responseHandler)
{
this.request = request;
this.responseHandler = responseHandler;
}
void newAttempt(final HttpResponseFuture future, final HttpServiceAttempt attempt, URI uri, final int attemptsLeft)
{
synchronized (subFutureLock) {
this.attempt = attempt;
this.subFuture = future;
this.uri = uri;
}
final RetryFuture retryFuture = this;
final Request request = this.request;
final ResponseHandler responseHandler = this.responseHandler;
Futures.addCallback(future, new FutureCallback()
{
@Override
public void onSuccess(T result)
{
attempt.markGood();
set(result);
}
@Override
public void onFailure(Throwable t)
{
if (t instanceof InnerHandlerException) {
InnerHandlerException innerHandlerException = (InnerHandlerException) t;
attempt.markBad(innerHandlerException.getFailureCategory(), innerHandlerException.getHandlerCategory());
setException(t.getCause());
}
else if (t instanceof FailureStatusException) {
attempt.markBad(((FailureStatusException) t).getFailureCategory());
//noinspection unchecked
set((T) ((FailureStatusException) t).result);
}
else if (t instanceof RetryException) {
attempt.markBad(((RetryException) t).getFailureCategory());
TraceToken traceToken = getCurrentTraceToken();
synchronized (subFutureLock) {
Duration backoff = attemptBackoffPolicy.backoff(previousBackoff);
ScheduledFuture> scheduledFuture = retryExecutor.schedule(() -> {
try (TraceTokenScope scope = registerTraceToken(traceToken)){
synchronized (subFutureLock) {
HttpServiceAttempt nextAttempt;
try {
nextAttempt = attempt.next();
previousBackoff = backoff;
attemptBackoffPolicy = attemptBackoffPolicy.nextAttempt();
}
catch (RuntimeException e1) {
try {
set(responseHandler.handleException(request, e1));
}
catch (Exception e2) {
setException(e2);
}
return;
}
try {
attemptQuery(retryFuture, request, responseHandler, nextAttempt, attemptsLeft);
}
catch (RuntimeException e1) {
setException(e1);
}
}
}
}, backoff.roundTo(MILLISECONDS), MILLISECONDS);
subFuture = new RetryDelayFuture<>(scheduledFuture, attempt);
}
}
}
}, directExecutor());
}
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
if (super.cancel(mayInterruptIfRunning)) {
// todo attempt.cancel() ?
synchronized (subFutureLock) {
subFuture.cancel(mayInterruptIfRunning);
}
return true;
}
return false;
}
@Override
public String getState()
{
synchronized (subFutureLock) {
return format("Attempt %s to %s: %s", attempt, uri, subFuture.getState());
}
}
}
private static class ImmediateHttpResponseFuture
extends AbstractFuture
implements HttpResponseFuture
{
private final T result;
ImmediateHttpResponseFuture(T result)
{
this.result = result;
set(result);
}
@Override
public String getState()
{
return "Succeeded with result " + result;
}
}
private static class ImmediateFailedHttpResponseFuture
extends AbstractFuture
implements HttpResponseFuture
{
private final E exception;
ImmediateFailedHttpResponseFuture(E exception)
{
this.exception = exception;
setException(exception);
}
@Override
public String getState()
{
return "Failed with exception " + exception;
}
}
private static class RetryDelayFuture
extends AbstractFuture
implements HttpResponseFuture
{
private final ScheduledFuture> scheduledFuture;
private final HttpServiceAttempt attempt;
public RetryDelayFuture(ScheduledFuture> scheduledFuture, HttpServiceAttempt attempt)
{
this.scheduledFuture = requireNonNull(scheduledFuture, "scheduledFuture is null");
this.attempt = requireNonNull(attempt, "attempt is null");
}
@Override
public String getState()
{
return format("Delaying for retry after attempt %s", attempt);
}
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
return scheduledFuture.cancel(mayInterruptIfRunning);
}
}
}