1 /*
2  * Copyright 2015 Google LLC
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *       http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.cloud;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 
21 import com.google.api.core.BetaApi;
22 import com.google.api.gax.retrying.ResultRetryAlgorithm;
23 import com.google.api.gax.retrying.TimedAttemptSettings;
24 import com.google.common.annotations.VisibleForTesting;
25 import com.google.common.base.Preconditions;
26 import com.google.common.collect.ImmutableList;
27 import com.google.common.collect.ImmutableSet;
28 import com.google.common.collect.Sets;
29 import java.io.Serializable;
30 import java.lang.reflect.Method;
31 import java.util.Objects;
32 import java.util.Set;
33 import java.util.concurrent.Callable;
34 
35 /** Exception retry algorithm implementation used by {@link RetryHelper}. */
36 @BetaApi
37 public final class ExceptionHandler implements ResultRetryAlgorithm<Object>, Serializable {
38 
39   private static final long serialVersionUID = -2460707015779532919L;
40 
41   private static final ExceptionHandler DEFAULT_INSTANCE =
42       newBuilder().retryOn(Exception.class).abortOn(RuntimeException.class).build();
43 
44   private final ImmutableList<Interceptor> interceptors;
45   private final ImmutableSet<Class<? extends Exception>> retriableExceptions;
46   private final ImmutableSet<Class<? extends Exception>> nonRetriableExceptions;
47   private final Set<RetryInfo> retryInfo = Sets.newHashSet();
48 
49   public interface Interceptor extends Serializable {
50 
51     enum RetryResult {
52       NO_RETRY,
53       RETRY,
54       CONTINUE_EVALUATION;
55     }
56 
57     /**
58      * This method is called before exception evaluation and could short-circuit the process.
59      *
60      * @param exception the exception that is being evaluated
61      * @return {@link RetryResult} to indicate if the exception should be ignored ( {@link
62      *     RetryResult#RETRY}), propagated ({@link RetryResult#NO_RETRY}), or evaluation should
63      *     proceed ({@link RetryResult#CONTINUE_EVALUATION}).
64      */
beforeEval(Exception exception)65     RetryResult beforeEval(Exception exception);
66 
67     /**
68      * This method is called after the evaluation and could alter its result.
69      *
70      * @param exception the exception that is being evaluated
71      * @param retryResult the result of the evaluation so far
72      * @return {@link RetryResult} to indicate if the exception should be ignored ( {@link
73      *     RetryResult#RETRY}), propagated ({@link RetryResult#NO_RETRY}), or evaluation should
74      *     proceed ({@link RetryResult#CONTINUE_EVALUATION}).
75      */
afterEval(Exception exception, RetryResult retryResult)76     RetryResult afterEval(Exception exception, RetryResult retryResult);
77   }
78 
79   /** ExceptionHandler builder. */
80   public static class Builder {
81 
82     private final ImmutableList.Builder<Interceptor> interceptors = ImmutableList.builder();
83     private final ImmutableSet.Builder<Class<? extends Exception>> retriableExceptions =
84         ImmutableSet.builder();
85     private final ImmutableSet.Builder<Class<? extends Exception>> nonRetriableExceptions =
86         ImmutableSet.builder();
87 
Builder()88     private Builder() {}
89 
90     /**
91      * Adds the exception handler interceptors. Call order will be maintained.
92      *
93      * @param interceptors the interceptors for this exception handler
94      * @return the Builder for chaining
95      */
addInterceptors(Interceptor... interceptors)96     public Builder addInterceptors(Interceptor... interceptors) {
97       for (Interceptor interceptor : interceptors) {
98         this.interceptors.add(interceptor);
99       }
100       return this;
101     }
102 
103     /**
104      * Add the exceptions to ignore/retry-on.
105      *
106      * @param exceptions retry should continue when such exceptions are thrown
107      * @return the Builder for chaining
108      */
109     @SafeVarargs
retryOn(Class<? extends Exception>.... exceptions)110     public final Builder retryOn(Class<? extends Exception>... exceptions) {
111       for (Class<? extends Exception> exception : exceptions) {
112         retriableExceptions.add(checkNotNull(exception));
113       }
114       return this;
115     }
116 
117     /**
118      * Adds the exceptions to abort on.
119      *
120      * @param exceptions retry should abort when such exceptions are thrown
121      * @return the Builder for chaining
122      */
123     @SafeVarargs
abortOn(Class<? extends Exception>.... exceptions)124     public final Builder abortOn(Class<? extends Exception>... exceptions) {
125       for (Class<? extends Exception> exception : exceptions) {
126         nonRetriableExceptions.add(checkNotNull(exception));
127       }
128       return this;
129     }
130 
131     /** Returns a new ExceptionHandler instance. */
build()132     public ExceptionHandler build() {
133       return new ExceptionHandler(this);
134     }
135   }
136 
137   @VisibleForTesting
138   static final class RetryInfo implements Serializable {
139 
140     private static final long serialVersionUID = -4264634837841455974L;
141     private final Class<? extends Exception> exception;
142     private final Interceptor.RetryResult retry;
143     private final Set<RetryInfo> children = Sets.newHashSet();
144 
RetryInfo(Class<? extends Exception> exception, Interceptor.RetryResult retry)145     RetryInfo(Class<? extends Exception> exception, Interceptor.RetryResult retry) {
146       this.exception = checkNotNull(exception);
147       this.retry = checkNotNull(retry);
148     }
149 
150     @Override
hashCode()151     public int hashCode() {
152       return exception.hashCode();
153     }
154 
155     @Override
equals(Object obj)156     public boolean equals(Object obj) {
157       if (obj == this) {
158         return true;
159       }
160       if (!(obj instanceof RetryInfo)) {
161         return false;
162       }
163       // We only care about exception in equality as we allow only one instance per exception
164       return ((RetryInfo) obj).exception.equals(exception);
165     }
166   }
167 
ExceptionHandler(Builder builder)168   private ExceptionHandler(Builder builder) {
169     interceptors = builder.interceptors.build();
170     retriableExceptions = builder.retriableExceptions.build();
171     nonRetriableExceptions = builder.nonRetriableExceptions.build();
172     Preconditions.checkArgument(
173         Sets.intersection(retriableExceptions, nonRetriableExceptions).isEmpty(),
174         "Same exception was found in both retryable and non-retryable sets");
175     for (Class<? extends Exception> exception : retriableExceptions) {
176       addRetryInfo(new RetryInfo(exception, Interceptor.RetryResult.RETRY), retryInfo);
177     }
178     for (Class<? extends Exception> exception : nonRetriableExceptions) {
179       addRetryInfo(new RetryInfo(exception, Interceptor.RetryResult.NO_RETRY), retryInfo);
180     }
181   }
182 
addRetryInfo(RetryInfo retryInfo, Set<RetryInfo> dest)183   private static void addRetryInfo(RetryInfo retryInfo, Set<RetryInfo> dest) {
184     for (RetryInfo current : dest) {
185       if (current.exception.isAssignableFrom(retryInfo.exception)) {
186         addRetryInfo(retryInfo, current.children);
187         return;
188       }
189       if (retryInfo.exception.isAssignableFrom(current.exception)) {
190         retryInfo.children.add(current);
191       }
192     }
193     dest.removeAll(retryInfo.children);
194     dest.add(retryInfo);
195   }
196 
findMostSpecificRetryInfo( Set<RetryInfo> retryInfo, Class<? extends Exception> exception)197   private static RetryInfo findMostSpecificRetryInfo(
198       Set<RetryInfo> retryInfo, Class<? extends Exception> exception) {
199     for (RetryInfo current : retryInfo) {
200       if (current.exception.isAssignableFrom(exception)) {
201         RetryInfo match = findMostSpecificRetryInfo(current.children, exception);
202         return match == null ? current : match;
203       }
204     }
205     return null;
206   }
207 
208   // called for Class<? extends Callable>, therefore a "call" method must be found.
getCallableMethod(Class<?> clazz)209   private static Method getCallableMethod(Class<?> clazz) {
210     try {
211       return clazz.getDeclaredMethod("call");
212     } catch (NoSuchMethodException e) {
213       // check parent
214       return getCallableMethod(clazz.getSuperclass());
215     } catch (SecurityException e) {
216       // This should never happen
217       throw new IllegalStateException("Unexpected exception", e);
218     }
219   }
220 
verifyCaller(Callable<?> callable)221   void verifyCaller(Callable<?> callable) {
222     Method callMethod = getCallableMethod(callable.getClass());
223     for (Class<?> exceptionOrError : callMethod.getExceptionTypes()) {
224       Preconditions.checkArgument(
225           Exception.class.isAssignableFrom(exceptionOrError),
226           "Callable method exceptions must be derived from Exception");
227       @SuppressWarnings("unchecked")
228       Class<? extends Exception> exception = (Class<? extends Exception>) exceptionOrError;
229       Preconditions.checkArgument(
230           findMostSpecificRetryInfo(retryInfo, exception) != null,
231           "Declared exception '" + exception + "' is not covered by exception handler");
232     }
233   }
234 
235   @Override
shouldRetry(Throwable prevThrowable, Object prevResponse)236   public boolean shouldRetry(Throwable prevThrowable, Object prevResponse) {
237     if (!(prevThrowable instanceof Exception)) {
238       return false;
239     }
240     Exception ex = (Exception) prevThrowable;
241     for (Interceptor interceptor : interceptors) {
242       Interceptor.RetryResult retryResult = checkNotNull(interceptor.beforeEval(ex));
243       if (retryResult != Interceptor.RetryResult.CONTINUE_EVALUATION) {
244         return retryResult == Interceptor.RetryResult.RETRY;
245       }
246     }
247     RetryInfo retryInfo = findMostSpecificRetryInfo(this.retryInfo, ex.getClass());
248     Interceptor.RetryResult retryResult =
249         retryInfo == null ? Interceptor.RetryResult.NO_RETRY : retryInfo.retry;
250     for (Interceptor interceptor : interceptors) {
251       Interceptor.RetryResult interceptorRetry =
252           checkNotNull(interceptor.afterEval(ex, retryResult));
253       if (interceptorRetry != Interceptor.RetryResult.CONTINUE_EVALUATION) {
254         retryResult = interceptorRetry;
255       }
256     }
257     return retryResult == Interceptor.RetryResult.RETRY;
258   }
259 
260   @Override
createNextAttempt( Throwable prevThrowable, Object prevResponse, TimedAttemptSettings prevSettings)261   public TimedAttemptSettings createNextAttempt(
262       Throwable prevThrowable, Object prevResponse, TimedAttemptSettings prevSettings) {
263     // Return null to indicate that this implementation does not provide any specific attempt
264     // settings, so by default the TimedRetryAlgorithm options can be used instead.
265     return null;
266   }
267 
268   @Override
hashCode()269   public int hashCode() {
270     return Objects.hash(interceptors, retriableExceptions, nonRetriableExceptions, retryInfo);
271   }
272 
273   @Override
equals(Object obj)274   public boolean equals(Object obj) {
275     if (obj == this) {
276       return true;
277     }
278     if (!(obj instanceof ExceptionHandler)) {
279       return false;
280     }
281     ExceptionHandler other = (ExceptionHandler) obj;
282     return Objects.equals(interceptors, other.interceptors)
283         && Objects.equals(retriableExceptions, other.retriableExceptions)
284         && Objects.equals(nonRetriableExceptions, other.nonRetriableExceptions)
285         && Objects.equals(retryInfo, other.retryInfo);
286   }
287 
288   /** Returns an instance which retry any checked exception and abort on any runtime exception. */
getDefaultInstance()289   public static ExceptionHandler getDefaultInstance() {
290     return DEFAULT_INSTANCE;
291   }
292 
newBuilder()293   public static Builder newBuilder() {
294     return new Builder();
295   }
296 }
297