// Copyright 2016 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.common.base.Preconditions; import java.lang.reflect.InvocationTargetException; import java.util.concurrent.Callable; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utility class for retrying operations. * * <p>It supports two custom handlers: {@code ExceptionChecker} and {@code WaitStrategy<V>} * that will be used to determine whether the operation's exception retriable, and calculate the * wait time before retry. * * @param <V> return value of the closure that is being run with retires. */ public class ApiRetryHelper<V> { private static final Logger logger = LoggerFactory.getLogger(ApiRetryHelper.class); @Nullable private final Long clientCustomerId; // E.g., RDS doesn't require CID. private final Callable<V> callable; private final String actionDescription; // Short description of the action. private final ApiRetryStrategy retryStrategy; public ApiRetryHelper( Long clientCustomerId, Callable<V> callable, String actionDescription, ApiRetryStrategy retryStrategy) { this.clientCustomerId = clientCustomerId; this.callable = Preconditions.checkNotNull(callable, "Argument 'callable' cannot be null."); this.actionDescription = Preconditions.checkNotNull( actionDescription, "Argument 'actionDescription' cannot be null."); this.retryStrategy = Preconditions.checkNotNull(retryStrategy, "Argument 'retryStrategy' cannot be null."); } /** * Invoke the AdWords API call with retry logic. * * @return the returned result of the callable other than AlertProcessingException occurs, the * thread is interrupted during waiting, or all retries are exhausted. */ public V callWithRetries() throws ApiInvocationException { V result = null; Throwable lastError = null; for (int kthAttempt = 1; retryStrategy.canDoThisAttempt(kthAttempt); ++kthAttempt) { // Wait if the previous attempt failed. long waitForMillis = retryStrategy.calcWaitTimeBeforeCall(clientCustomerId, kthAttempt, lastError); if (waitForMillis > 0) { logger.info( "Thread \"{}\" is sleeping for {} millis.", Thread.currentThread().getName(), waitForMillis); try { Thread.sleep(waitForMillis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new ApiInvocationException( "InterruptedException occurs while waiting to " + actionDescription, e.getCause()); } } try { lastError = null; result = callable.call(); break; } catch (IllegalAccessException e) { throw new RateLimiterException("Illegal access to invoke: " + actionDescription, e); } catch (InvocationTargetException e) { lastError = e.getCause(); } catch (Exception e) { lastError = e; } // Check whether the error is retriable if (retryStrategy.shouldRetryOnError(clientCustomerId, lastError)) { logger.error( "Failed to {} at exception check, attempt #{}.", actionDescription, kthAttempt); } else { logger.error( "Failed to {} at exception check: encountered non-retriable {}, skip retry!", actionDescription, lastError.getClass().getName()); throw new ApiInvocationException("Encountered non-retriable exception.", lastError); } } if (result == null) { throw new ApiInvocationException( "Failed to " + actionDescription + " after all retries.", lastError); } return result; } }