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