/*
* Copyright 2016-present Facebook, 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.facebook.buck.slb;
import com.facebook.buck.counters.CounterRegistry;
import com.facebook.buck.counters.IntegerCounter;
import com.facebook.buck.event.BuckEventBus;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import okhttp3.Request;
public class RetryingHttpService implements HttpService {
public static final String COUNTER_CATEGORY = "buck_retry_service_counters";
private final HttpService decoratedService;
private final int maxNumberOfAttempts;
private final IntegerCounter successAfterRetryCountCounter;
private final IntegerCounter retryCountCounter;
private final IntegerCounter failAfterAllRetriesCountCounter;
// Currently when there's a cache miss, all the children nodes get immediately retried without
// any backoffs. We will do the same here for this initial implementation (and also to avoid
// adding extra latency during the retry policy).
public RetryingHttpService(
BuckEventBus eventBus, HttpService decoratedService, int maxNumberOfRetries) {
Preconditions.checkArgument(
maxNumberOfRetries >= 0,
"The max number of retries needs to be non-negative instead of: %s",
maxNumberOfRetries);
this.decoratedService = decoratedService;
this.maxNumberOfAttempts = maxNumberOfRetries + 1;
failAfterAllRetriesCountCounter =
new IntegerCounter(COUNTER_CATEGORY, "fail_after_all_retries_count", ImmutableMap.of());
successAfterRetryCountCounter =
new IntegerCounter(COUNTER_CATEGORY, "success_after_retry_count", ImmutableMap.of());
retryCountCounter = new IntegerCounter(COUNTER_CATEGORY, "retry_count", ImmutableMap.of());
eventBus.post(
new CounterRegistry.AsyncCounterRegistrationEvent(
ImmutableList.of(
failAfterAllRetriesCountCounter,
successAfterRetryCountCounter,
retryCountCounter)));
}
@Override
public HttpResponse makeRequest(String path, Request.Builder request) throws IOException {
List<IOException> allExceptions = new ArrayList<>();
for (int retryCount = 0; retryCount < maxNumberOfAttempts; retryCount++) {
try {
if (retryCount > 0) {
retryCountCounter.inc();
}
HttpResponse response = decoratedService.makeRequest(path, request);
if (retryCount > 0) {
successAfterRetryCountCounter.inc();
}
return response;
} catch (IOException exception) {
allExceptions.add(exception);
}
}
if (maxNumberOfAttempts > 0) {
failAfterAllRetriesCountCounter.inc();
}
throw new RetryingHttpServiceException(allExceptions);
}
@Override
public void close() {
decoratedService.close();
}
public static class RetryingHttpServiceException extends IOException {
public RetryingHttpServiceException(List<IOException> allExceptions) {
super(generateMessage(allExceptions), allExceptions.get(allExceptions.size() - 1));
}
@Override
public String toString() {
return String.format("RetryingHttpServiceException{%s}", getMessage());
}
private static String generateMessage(List<IOException> exceptions) {
StringBuilder builder = new StringBuilder();
builder.append(
String.format("Too many fails after %1$d retries. Exceptions:", exceptions.size()));
for (int i = 0; i < exceptions.size(); ++i) {
builder.append(String.format(" %d:[%s]", i, exceptions.get(i).toString()));
}
return builder.toString();
}
}
}