// Copyright 2017 Google Inc. All Rights Reserved. // // 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.google.api.ads.adwords.extension.ratelimiter; import com.google.api.ads.adwords.lib.utils.DetailedReportDownloadResponseException; import com.google.common.base.Strings; import java.net.HttpURLConnection; import javax.annotation.Nullable; /** * The {@link ApiRetryStrategy} implementation for report downing bucket. * * <p>To change the default configuration, set the system properties {@value * #MAX_ATTEMPTS_ON_RATE_EXCEEDED_ERROR_PROPERTY} and {@value * #BACKOFF_INTERVAL_ON_RATE_EXCEEDED_ERROR_PROPERTY} <em>before</em> calling {@link #newInstance()} * for the first time. */ public final class ApiReportingRetryStrategy implements ApiRetryStrategy { // Property for the maximum number of attempts on rate limit error. static final String MAX_ATTEMPTS_ON_RATE_EXCEEDED_ERROR_PROPERTY = "com.google.api.ads.adwords.extension.ratelimiter.ApiReportingRetryStrategy.maxAttemptsOnRateExceededError"; private static final int MAX_ATTEMPTS_ON_RATE_EXCEEDED_ERROR_DEFAULT = 3; // Property for the exponential backoff interval (in milliseconds) before retrying on rate limit // error. static final String BACKOFF_INTERVAL_ON_RATE_EXCEEDED_ERROR_PROPERTY = "com.google.api.ads.adwords.extension.ratelimiter.ApiReportingRetryStrategy.backoffIntervalOnRateExceededError"; private static final int BACKOFF_INTERVAL_ON_RATE_EXCEEDED_ERROR_DEFAULT = 1000 * 5; // Thread-safe helper for calculating {@link ApiReportingRetryStrategy} configuration. private static final class ConfigCalculator { private static final int MAX_ATTEMPTS_ON_RATE_EXCEEDED_ERROR = ConfigUtil.getIntConfigValue( MAX_ATTEMPTS_ON_RATE_EXCEEDED_ERROR_PROPERTY, MAX_ATTEMPTS_ON_RATE_EXCEEDED_ERROR_DEFAULT); private static final int BACKOFF_INTERVAL_ON_RATE_EXCEEDED_ERROR = ConfigUtil.getIntConfigValue( BACKOFF_INTERVAL_ON_RATE_EXCEEDED_ERROR_PROPERTY, BACKOFF_INTERVAL_ON_RATE_EXCEEDED_ERROR_DEFAULT); } // The string to check against for rate limit error. private static final String RATE_EXCEEDED_ERROR = "RateExceededError"; // Number of attempts on rate limit error. private final int maxAttemptsOnRateExceededError; // Number of exponential backoff interval (in milliseconds) before retrying on rate limit error. private final int backoffIntervalOnRateExceededError; private ApiReportingRetryStrategy() { this.maxAttemptsOnRateExceededError = ConfigCalculator.MAX_ATTEMPTS_ON_RATE_EXCEEDED_ERROR; this.backoffIntervalOnRateExceededError = ConfigCalculator.BACKOFF_INTERVAL_ON_RATE_EXCEEDED_ERROR; } public static ApiReportingRetryStrategy newInstance() { return new ApiReportingRetryStrategy(); } @Override public boolean canDoThisAttempt(int kthAttempt) { return kthAttempt <= maxAttemptsOnRateExceededError; } @Override public boolean shouldRetryOnError(@Nullable Long clientCustomerId, Throwable throwable) { // Do not care about clientCustomerId, just check RateExceededError. // By default it can retry (e.g., ReportException, ReportDownloadResponseException). boolean canRetry = true; if (throwable instanceof DetailedReportDownloadResponseException) { DetailedReportDownloadResponseException ex = (DetailedReportDownloadResponseException) throwable; int httpStatus = ex.getHttpStatus(); String errorText = ex.getErrorText(); // Rate limit error has httpStatus 400 and errorText containing "RateExceededError". boolean isRateLimitError = httpStatus == HttpURLConnection.HTTP_BAD_REQUEST && Strings.nullToEmpty(errorText).contains(RATE_EXCEEDED_ERROR); // Retry iff it's rate limit error. canRetry = isRateLimitError; } return canRetry; } @Override public long calcWaitTimeBeforeCall( @Nullable Long clientCutomerId, final int kthAttempt, Throwable throwable) { // Do not care about clientCustomerId and throwable, just do exponential backoff. return kthAttempt == 1 ? 0 : (long) Math.scalb(backoffIntervalOnRateExceededError, kthAttempt - 1); } }