diff --git a/src/main/java/org/springframework/retry/RetryCallback.java b/src/main/java/org/springframework/retry/RetryCallback.java index ed9e825..b21ca2f 100644 --- a/src/main/java/org/springframework/retry/RetryCallback.java +++ b/src/main/java/org/springframework/retry/RetryCallback.java @@ -33,5 +33,5 @@ public interface RetryCallback { * @return the result of the successful operation. * @throws Exception if processing fails */ - T doWithRetry(RetryContext context) throws Exception; + T doWithRetry(RetryContext context) throws Throwable; } diff --git a/src/main/java/org/springframework/retry/RetryOperations.java b/src/main/java/org/springframework/retry/RetryOperations.java index 55fc0b3..dd2bc04 100644 --- a/src/main/java/org/springframework/retry/RetryOperations.java +++ b/src/main/java/org/springframework/retry/RetryOperations.java @@ -35,8 +35,9 @@ public interface RetryOperations { * invocation. * @throws Exception any {@link Exception} raised by the * {@link RetryCallback} upon unsuccessful retry. + * @throws Throwable */ - T execute(RetryCallback retryCallback) throws Exception; + T execute(RetryCallback retryCallback) throws Throwable; /** * Execute the supplied {@link RetryCallback} with a fallback on exhausted @@ -48,7 +49,7 @@ public interface RetryOperations { * @throws Exception any {@link Exception} raised by the * {@link RecoveryCallback} upon unsuccessful retry. */ - T execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback) throws Exception; + T execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback) throws Throwable; /** * A simple stateful retry. Execute the supplied {@link RetryCallback} with @@ -68,7 +69,7 @@ public interface RetryOperations { * @throws ExhaustedRetryException if the last attempt for this state has * already been reached */ - T execute(RetryCallback retryCallback, RetryState retryState) throws Exception, ExhaustedRetryException; + T execute(RetryCallback retryCallback, RetryState retryState) throws Throwable, ExhaustedRetryException; /** * A stateful retry with a recovery path. Execute the supplied @@ -84,6 +85,6 @@ public interface RetryOperations { * {@link RecoveryCallback} upon unsuccessful retry. */ T execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback, RetryState retryState) - throws Exception; + throws Throwable; } diff --git a/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java index fcda5cb..a7db158 100644 --- a/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/interceptor/RetryOperationsInterceptor.java @@ -18,7 +18,6 @@ import java.util.Arrays; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.ProxyMethodInvocation; -import org.springframework.retry.ExhaustedRetryException; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; @@ -27,14 +26,17 @@ import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; /** - * A {@link MethodInterceptor} that can be used to automatically retry calls to a method on a service if it fails. The - * injected {@link RetryOperations} is used to control the number of retries. By default it will retry a fixed number of - * times, according to the defaults in {@link RetryTemplate}.
+ * A {@link MethodInterceptor} that can be used to automatically retry calls to a method + * on a service if it fails. The injected {@link RetryOperations} is used to control the + * number of retries. By default it will retry a fixed number of times, according to the + * defaults in {@link RetryTemplate}.
* - * Hint about transaction boundaries. If you want to retry a failed transaction you need to make sure that the - * transaction boundary is inside the retry, otherwise the successful attempt will roll back with the whole transaction. - * If the method being intercepted is also transactional, then use the ordering hints in the advice declarations to - * ensure that this one is before the transaction interceptor in the advice chain. + * Hint about transaction boundaries. If you want to retry a failed transaction you need + * to make sure that the transaction boundary is inside the retry, otherwise the + * successful attempt will roll back with the whole transaction. If the method being + * intercepted is also transactional, then use the ordering hints in the advice + * declarations to ensure that this one is before the transaction interceptor in the + * advice chain. * * @author Rob Harrop * @author Dave Syer @@ -60,13 +62,15 @@ public class RetryOperationsInterceptor implements MethodInterceptor { public Object doWithRetry(RetryContext context) throws Exception { /* - * If we don't copy the invocation carefully it won't keep a reference to the other interceptors in the - * chain. We don't have a choice here but to specialise to ReflectiveMethodInvocation (but how often - * would another implementation come along?). + * If we don't copy the invocation carefully it won't keep a reference to + * the other interceptors in the chain. We don't have a choice here but to + * specialise to ReflectiveMethodInvocation (but how often would another + * implementation come along?). */ if (invocation instanceof ProxyMethodInvocation) { try { - return ((ProxyMethodInvocation) invocation).invocableClone().proceed(); + return ((ProxyMethodInvocation) invocation).invocableClone() + .proceed(); } catch (Exception e) { throw e; } catch (Error e) { @@ -83,7 +87,8 @@ public class RetryOperationsInterceptor implements MethodInterceptor { }; if (recoverer != null) { - ItemRecovererCallback recoveryCallback = new ItemRecovererCallback(invocation.getArguments(), recoverer); + ItemRecovererCallback recoveryCallback = new ItemRecovererCallback( + invocation.getArguments(), recoverer); return this.retryOperations.execute(retryCallback, recoveryCallback); } @@ -104,16 +109,14 @@ public class RetryOperationsInterceptor implements MethodInterceptor { /** * @param args the item that failed. */ - private ItemRecovererCallback(Object[] args, MethodInvocationRecoverer recoverer) { + private ItemRecovererCallback(Object[] args, + MethodInvocationRecoverer recoverer) { this.args = Arrays.asList(args).toArray(); this.recoverer = recoverer; } public Object recover(RetryContext context) { - if (recoverer != null) { - return recoverer.recover(args, context.getLastThrowable()); - } - throw new ExhaustedRetryException("Retry was exhausted but there was no recovery path."); + return recoverer.recover(args, context.getLastThrowable()); } } diff --git a/src/main/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptor.java index badf6d3..e896686 100644 --- a/src/main/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptor.java @@ -22,7 +22,6 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.retry.ExhaustedRetryException; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; @@ -35,20 +34,20 @@ import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * A {@link MethodInterceptor} that can be used to automatically retry calls to - * a method on a service if it fails. The argument to the service method is - * treated as an item to be remembered in case the call fails. So the retry - * operation is stateful, and the item that failed is tracked by its unique key - * (via {@link MethodArgumentsKeyGenerator}) until the retry is exhausted, at - * which point the {@link MethodInvocationRecoverer} is called.
+ * A {@link MethodInterceptor} that can be used to automatically retry calls to a method + * on a service if it fails. The argument to the service method is treated as an item to + * be remembered in case the call fails. So the retry operation is stateful, and the item + * that failed is tracked by its unique key (via {@link MethodArgumentsKeyGenerator}) + * until the retry is exhausted, at which point the {@link MethodInvocationRecoverer} is + * called.
* - * The main use case for this is where the service is transactional, via a - * transaction interceptor on the interceptor chain. In this case the retry (and - * recovery on exhausted) always happens in a new transaction.
+ * The main use case for this is where the service is transactional, via a transaction + * interceptor on the interceptor chain. In this case the retry (and recovery on + * exhausted) always happens in a new transaction.
* - * The injected {@link RetryOperations} is used to control the number of - * retries. By default it will retry a fixed number of times, according to the - * defaults in {@link RetryTemplate}.
+ * The injected {@link RetryOperations} is used to control the number of retries. By + * default it will retry a fixed number of times, according to the defaults in + * {@link RetryTemplate}.
* * @author Dave Syer */ @@ -80,13 +79,10 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor { } /** - * Public setter for the {@link MethodInvocationRecoverer} to use if the - * retry is exhausted. The recoverer should be able to return an object of - * the same type as the target object because its return value will be used - * to return to the caller in the case of a recovery.
- * - * If no recoverer is set then an exhausted retry will result in an - * {@link ExhaustedRetryException}. + * Public setter for the {@link MethodInvocationRecoverer} to use if the retry is + * exhausted. The recoverer should be able to return an object of the same type as the + * target object because its return value will be used to return to the caller in the + * case of a recovery.
* * @param recoverer the {@link MethodInvocationRecoverer} to set */ @@ -99,53 +95,57 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor { } /** - * Public setter for the {@link NewMethodArgumentsIdentifier}. Only set this - * if the arguments to the intercepted method can be inspected to find out - * if they have never been processed before. - * @param newMethodArgumentsIdentifier the - * {@link NewMethodArgumentsIdentifier} to set + * Public setter for the {@link NewMethodArgumentsIdentifier}. Only set this if the + * arguments to the intercepted method can be inspected to find out if they have never + * been processed before. + * @param newMethodArgumentsIdentifier the {@link NewMethodArgumentsIdentifier} to set */ - public void setNewItemIdentifier(NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) { + public void setNewItemIdentifier( + NewMethodArgumentsIdentifier newMethodArgumentsIdentifier) { this.newMethodArgumentsIdentifier = newMethodArgumentsIdentifier; } /** - * Wrap the method invocation in a stateful retry with the policy and other - * helpers provided. If there is a failure the exception will generally be - * re-thrown. The only time it is not re-thrown is when retry is exhausted - * and the recovery path is taken (though the - * {@link MethodInvocationRecoverer} provided if there is one). In that case - * the value returned from the method invocation will be the value returned - * by the recoverer (so the return type for that should be the same as the - * intercepted method). + * Wrap the method invocation in a stateful retry with the policy and other helpers + * provided. If there is a failure the exception will generally be re-thrown. The only + * time it is not re-thrown is when retry is exhausted and the recovery path is taken + * (though the {@link MethodInvocationRecoverer} provided if there is one). In that + * case the value returned from the method invocation will be the value returned by + * the recoverer (so the return type for that should be the same as the intercepted + * method). * * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) * @see MethodInvocationRecoverer#recover(Object[], Throwable) * - * @throws ExhaustedRetryException if the retry is exhausted and no - * {@link MethodInvocationRecoverer} is provided. */ public Object invoke(final MethodInvocation invocation) throws Throwable { - logger.debug("Executing proxied method in stateful retry: " + invocation.getStaticPart() + "(" + logger.debug("Executing proxied method in stateful retry: " + + invocation.getStaticPart() + "(" + ObjectUtils.getIdentityHexString(invocation) + ")"); Object[] args = invocation.getArguments(); - Assert.state(args.length > 0, "Stateful retry applied to method that takes no arguments: " - + invocation.getStaticPart()); + Assert.state( + args.length > 0, + "Stateful retry applied to method that takes no arguments: " + + invocation.getStaticPart()); Object arg = args; if (args.length == 1) { arg = args[0]; } final Object item = arg; - RetryState retryState = new DefaultRetryState(keyGenerator != null ? keyGenerator.getKey(args) : item, - newMethodArgumentsIdentifier != null ? newMethodArgumentsIdentifier.isNew(args) : false); + RetryState retryState = new DefaultRetryState( + keyGenerator != null ? keyGenerator.getKey(args) : item, + newMethodArgumentsIdentifier != null ? newMethodArgumentsIdentifier + .isNew(args) : false); - Object result = retryOperations.execute(new MethodInvocationRetryCallback(invocation), - new ItemRecovererCallback(args, recoverer), retryState); + Object result = retryOperations.execute(new MethodInvocationRetryCallback( + invocation), recoverer != null ? new ItemRecovererCallback(args, + recoverer) : null, retryState); - logger.debug("Exiting proxied method in stateful retry with result: (" + result + ")"); + logger.debug("Exiting proxied method in stateful retry with result: (" + result + + ")"); return result; @@ -155,7 +155,8 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor { * @author Dave Syer * */ - private static final class MethodInvocationRetryCallback implements RetryCallback { + private static final class MethodInvocationRetryCallback implements + RetryCallback { /** * */ @@ -171,14 +172,11 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor { public Object doWithRetry(RetryContext context) throws Exception { try { return invocation.proceed(); - } - catch (Exception e) { + } catch (Exception e) { throw e; - } - catch (Error e) { + } catch (Error e) { throw e; - } - catch (Throwable e) { + } catch (Throwable e) { throw new IllegalStateException(e); } } @@ -197,16 +195,14 @@ public class StatefulRetryOperationsInterceptor implements MethodInterceptor { /** * @param args the item that failed. */ - private ItemRecovererCallback(Object[] args, MethodInvocationRecoverer recoverer) { + private ItemRecovererCallback(Object[] args, + MethodInvocationRecoverer recoverer) { this.args = Arrays.asList(args).toArray(); this.recoverer = recoverer; } public Object recover(RetryContext context) { - if (recoverer != null) { - return recoverer.recover(args, context.getLastThrowable()); - } - throw new ExhaustedRetryException("Retry was exhausted but there was no recovery path."); + return recoverer.recover(args, context.getLastThrowable()); } } diff --git a/src/main/java/org/springframework/retry/support/RetrySimulator.java b/src/main/java/org/springframework/retry/support/RetrySimulator.java index 3a5fb9d..31f84b8 100644 --- a/src/main/java/org/springframework/retry/support/RetrySimulator.java +++ b/src/main/java/org/springframework/retry/support/RetrySimulator.java @@ -89,7 +89,7 @@ public class RetrySimulator { template.execute(new FailingRetryCallback()); } catch(FailingRetryException e) { - } catch(Exception e) { + } catch(Throwable e) { throw new RuntimeException("Unexpected exception", e); } diff --git a/src/main/java/org/springframework/retry/support/RetryTemplate.java b/src/main/java/org/springframework/retry/support/RetryTemplate.java index 81cc451..a163225 100644 --- a/src/main/java/org/springframework/retry/support/RetryTemplate.java +++ b/src/main/java/org/springframework/retry/support/RetryTemplate.java @@ -43,28 +43,24 @@ import org.springframework.retry.policy.RetryContextCache; import org.springframework.retry.policy.SimpleRetryPolicy; /** - * Template class that simplifies the execution of operations with retry - * semantics.
- * Retryable operations are encapsulated in implementations of the - * {@link RetryCallback} interface and are executed using one of the supplied - * execute methods.
+ * Template class that simplifies the execution of operations with retry semantics.
+ * Retryable operations are encapsulated in implementations of the {@link RetryCallback} + * interface and are executed using one of the supplied execute methods.
* - * By default, an operation is retried if is throws any {@link Exception} or - * subclass of {@link Exception}. This behaviour can be changed by using the + * By default, an operation is retried if is throws any {@link Exception} or subclass of + * {@link Exception}. This behaviour can be changed by using the * {@link #setRetryPolicy(RetryPolicy)} method.
* - * Also by default, each operation is retried for a maximum of three attempts - * with no back off in between. This behaviour can be configured using the - * {@link #setRetryPolicy(RetryPolicy)} and - * {@link #setBackOffPolicy(BackOffPolicy)} properties. The - * {@link org.springframework.retry.backoff.BackOffPolicy} controls how + * Also by default, each operation is retried for a maximum of three attempts with no back + * off in between. This behaviour can be configured using the + * {@link #setRetryPolicy(RetryPolicy)} and {@link #setBackOffPolicy(BackOffPolicy)} + * properties. The {@link org.springframework.retry.backoff.BackOffPolicy} controls how * long the pause is between each individual retry attempt.
* - * This class is thread-safe and suitable for concurrent access when executing - * operations and when performing configuration changes. As such, it is possible - * to change the number of retries on the fly, as well as the - * {@link BackOffPolicy} used and no in progress retryable operations will be - * affected. + * This class is thread-safe and suitable for concurrent access when executing operations + * and when performing configuration changes. As such, it is possible to change the number + * of retries on the fly, as well as the {@link BackOffPolicy} used and no in progress + * retryable operations will be affected. * * @author Rob Harrop * @author Dave Syer @@ -76,13 +72,23 @@ public class RetryTemplate implements RetryOperations { private volatile BackOffPolicy backOffPolicy = new NoBackOffPolicy(); - private volatile RetryPolicy retryPolicy = new SimpleRetryPolicy(3, Collections - ., Boolean> singletonMap(Exception.class, true)); + private volatile RetryPolicy retryPolicy = new SimpleRetryPolicy(3, + Collections., Boolean> singletonMap( + Exception.class, true)); private volatile RetryListener[] listeners = new RetryListener[0]; private RetryContextCache retryContextCache = new MapRetryContextCache(); + private boolean throwLastExceptionOnExhausted; + + /** + * @param throwLastExceptionOnExhausted the throwLastExceptionOnExhausted to set + */ + public void setThrowLastExceptionOnExhausted(boolean throwLastExceptionOnExhausted) { + this.throwLastExceptionOnExhausted = throwLastExceptionOnExhausted; + } + /** * Public setter for the {@link RetryContextCache}. * @@ -93,15 +99,15 @@ public class RetryTemplate implements RetryOperations { } /** - * Setter for listeners. The listeners are executed before and after a retry - * block (i.e. before and after all the attempts), and on an error (every - * attempt). + * Setter for listeners. The listeners are executed before and after a retry block + * (i.e. before and after all the attempts), and on an error (every attempt). * * @param listeners * @see RetryListener */ public void setListeners(RetryListener[] listeners) { - this.listeners = Arrays.asList(listeners).toArray(new RetryListener[listeners.length]); + this.listeners = Arrays.asList(listeners).toArray( + new RetryListener[listeners.length]); } /** @@ -135,68 +141,68 @@ public class RetryTemplate implements RetryOperations { } /** - * Keep executing the callback until it either succeeds or the policy - * dictates that we stop, in which case the most recent exception thrown by - * the callback will be rethrown. + * Keep executing the callback until it either succeeds or the policy dictates that we + * stop, in which case the most recent exception thrown by the callback will be + * rethrown. * * @see RetryOperations#execute(RetryCallback) * - * @throws TerminatedRetryException if the retry has been manually - * terminated by a listener. + * @throws TerminatedRetryException if the retry has been manually terminated by a + * listener. */ - public final T execute(RetryCallback retryCallback) throws Exception { + public final T execute(RetryCallback retryCallback) throws Throwable { return doExecute(retryCallback, null, null); } /** - * Keep executing the callback until it either succeeds or the policy - * dictates that we stop, in which case the recovery callback will be - * executed. + * Keep executing the callback until it either succeeds or the policy dictates that we + * stop, in which case the recovery callback will be executed. * * @see RetryOperations#execute(RetryCallback, RecoveryCallback) * - * @throws TerminatedRetryException if the retry has been manually - * terminated by a listener. + * @throws TerminatedRetryException if the retry has been manually terminated by a + * listener. */ - public final T execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback) throws Exception { + public final T execute(RetryCallback retryCallback, + RecoveryCallback recoveryCallback) throws Throwable { return doExecute(retryCallback, recoveryCallback, null); } /** - * Execute the callback once if the policy dictates that we can, re-throwing - * any exception encountered so that clients can re-present the same task - * later. + * Execute the callback once if the policy dictates that we can, re-throwing any + * exception encountered so that clients can re-present the same task later. * * @see RetryOperations#execute(RetryCallback, RetryState) * * @throws ExhaustedRetryException if the retry has been exhausted. */ - public final T execute(RetryCallback retryCallback, RetryState retryState) throws Exception, - ExhaustedRetryException { + public final T execute(RetryCallback retryCallback, RetryState retryState) + throws Throwable, ExhaustedRetryException { return doExecute(retryCallback, null, retryState); } /** - * Execute the callback once if the policy dictates that we can, re-throwing - * any exception encountered so that clients can re-present the same task - * later. + * Execute the callback once if the policy dictates that we can, re-throwing any + * exception encountered so that clients can re-present the same task later. * * @see RetryOperations#execute(RetryCallback, RetryState) */ - public final T execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback, - RetryState retryState) throws Exception, ExhaustedRetryException { + public final T execute(RetryCallback retryCallback, + RecoveryCallback recoveryCallback, RetryState retryState) + throws Throwable, ExhaustedRetryException { return doExecute(retryCallback, recoveryCallback, retryState); } /** - * Execute the callback once if the policy dictates that we can, otherwise - * execute the recovery callback. + * Execute the callback once if the policy dictates that we can, otherwise execute the + * recovery callback. * * @see RetryOperations#execute(RetryCallback, RecoveryCallback, RetryState) * @throws ExhaustedRetryException if the retry has been exhausted. */ - protected T doExecute(RetryCallback retryCallback, RecoveryCallback recoveryCallback, RetryState state) - throws Exception, ExhaustedRetryException { + protected T doExecute(RetryCallback retryCallback, + RecoveryCallback recoveryCallback, RetryState state) throws Throwable, + ExhaustedRetryException { RetryPolicy retryPolicy = this.retryPolicy; BackOffPolicy backOffPolicy = this.backOffPolicy; @@ -219,7 +225,8 @@ public class RetryTemplate implements RetryOperations { boolean running = doOpenInterceptors(retryCallback, context); if (!running) { - throw new TerminatedRetryException("Retry terminated abnormally by interceptor before first attempt"); + throw new TerminatedRetryException( + "Retry terminated abnormally by interceptor before first attempt"); } // Get or Start the backoff context... @@ -240,10 +247,10 @@ public class RetryTemplate implements RetryOperations { } /* - * We allow the whole loop to be skipped if the policy or context - * already forbid the first try. This is used in the case of - * external retry to allow a recovery in handleRetryExhausted - * without the callback processing (which would throw an exception). + * We allow the whole loop to be skipped if the policy or context already + * forbid the first try. This is used in the case of external retry to allow a + * recovery in handleRetryExhausted without the callback processing (which + * would throw an exception). */ while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) { @@ -253,8 +260,7 @@ public class RetryTemplate implements RetryOperations { // the close interceptors will not think we failed... lastException = null; return retryCallback.doWithRetry(context); - } - catch (Throwable e) { + } catch (Throwable e) { lastException = e; @@ -263,48 +269,49 @@ public class RetryTemplate implements RetryOperations { try { registerThrowable(retryPolicy, state, context, e); } catch (Exception ex) { - throw new TerminatedRetryException("Could not register throwable", ex); + throw new TerminatedRetryException( + "Could not register throwable", ex); } if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) { try { backOffPolicy.backOff(backOffContext); - } - catch (BackOffInterruptedException ex) { + } catch (BackOffInterruptedException ex) { lastException = e; // back off was prevented by another thread - fail // the retry - logger.debug("Abort retry because interrupted: count=" + context.getRetryCount()); + logger.debug("Abort retry because interrupted: count=" + + context.getRetryCount()); throw ex; } } logger.debug("Checking for rethrow: count=" + context.getRetryCount()); if (shouldRethrow(retryPolicy, context, state)) { - logger.debug("Rethrow in retry for policy: count=" + context.getRetryCount()); + logger.debug("Rethrow in retry for policy: count=" + + context.getRetryCount()); throw wrapIfNecessary(e); } } /* - * A stateful attempt that can retry should have rethrown the - * exception by now - i.e. we shouldn't get this far for a - * stateful attempt if it can retry. + * A stateful attempt that can retry should have rethrown the exception by + * now - i.e. we shouldn't get this far for a stateful attempt if it can + * retry. */ } logger.debug("Retry failed last attempt: count=" + context.getRetryCount()); if (context.isExhaustedOnly()) { - throw new ExhaustedRetryException("Retry exhausted after last attempt with no recovery path.", context - .getLastThrowable()); + rethrow(context, + "Retry exhausted after last attempt with no recovery path."); } return handleRetryExhausted(recoveryCallback, context, state); - } - finally { + } finally { close(retryPolicy, context, state, lastException == null); doCloseInterceptors(retryCallback, context, lastException); RetrySynchronizationManager.clear(); @@ -313,9 +320,9 @@ public class RetryTemplate implements RetryOperations { } /** - * Decide whether to proceed with the ongoing retry attempt. This method is - * called before the {@link RetryCallback} is executed, but after the - * backoff and open interceptors. + * Decide whether to proceed with the ongoing retry attempt. This method is called + * before the {@link RetryCallback} is executed, but after the backoff and open + * interceptors. * * @param retryPolicy the policy to apply * @param context the current retry context @@ -326,21 +333,21 @@ public class RetryTemplate implements RetryOperations { } /** - * Clean up the cache if necessary and close the context provided (if the - * flag indicates that processing was successful). + * Clean up the cache if necessary and close the context provided (if the flag + * indicates that processing was successful). * * @param context * @param state * @param succeeded */ - protected void close(RetryPolicy retryPolicy, RetryContext context, RetryState state, boolean succeeded) { + protected void close(RetryPolicy retryPolicy, RetryContext context, RetryState state, + boolean succeeded) { if (state != null) { if (succeeded) { retryContextCache.remove(state.getKey()); retryPolicy.close(context); } - } - else { + } else { retryPolicy.close(context); } } @@ -351,13 +358,15 @@ public class RetryTemplate implements RetryOperations { * @param context * @param e */ - protected void registerThrowable(RetryPolicy retryPolicy, RetryState state, RetryContext context, Throwable e) { + protected void registerThrowable(RetryPolicy retryPolicy, RetryState state, + RetryContext context, Throwable e) { if (state != null) { Object key = state.getKey(); if (context.getRetryCount() > 0 && !retryContextCache.containsKey(key)) { - throw new RetryException("Inconsistent state for failed item key: cache key has changed. " - + "Consider whether equals() or hashCode() for the key might be inconsistent, " - + "or if you need to supply a better key"); + throw new RetryException( + "Inconsistent state for failed item key: cache key has changed. " + + "Consider whether equals() or hashCode() for the key might be inconsistent, " + + "or if you need to supply a better key"); } retryContextCache.put(key, context); } @@ -365,12 +374,12 @@ public class RetryTemplate implements RetryOperations { } /** - * Delegate to the {@link RetryPolicy} having checked in the cache for an - * existing value if the state is not null. + * Delegate to the {@link RetryPolicy} having checked in the cache for an existing + * value if the state is not null. * * @param retryPolicy a {@link RetryPolicy} to delegate the context creation - * @return a retry context, either a new one or the one used last time the - * same state was encountered + * @return a retry context, either a new one or the one used last time the same state + * was encountered */ protected RetryContext open(RetryPolicy retryPolicy, RetryState state) { @@ -393,9 +402,10 @@ public class RetryTemplate implements RetryOperations { RetryContext context = retryContextCache.get(key); if (context == null) { if (retryContextCache.containsKey(key)) { - throw new RetryException("Inconsistent state for failed item: no history found. " - + "Consider whether equals() or hashCode() for the item might be inconsistent, " - + "or if you need to supply a better ItemKeyGenerator"); + throw new RetryException( + "Inconsistent state for failed item: no history found. " + + "Consider whether equals() or hashCode() for the item might be inconsistent, " + + "or if you need to supply a better ItemKeyGenerator"); } // The cache could have been expired in between calls to // containsKey(), so we have to live with this: @@ -415,19 +425,19 @@ public class RetryTemplate implements RetryOperations { } /** - * Actions to take after final attempt has failed. If there is state clean - * up the cache. If there is a recovery callback, execute that and return - * its result. Otherwise throw an exception. + * Actions to take after final attempt has failed. If there is state clean up the + * cache. If there is a recovery callback, execute that and return its result. + * Otherwise throw an exception. * * @param recoveryCallback the callback for recovery (might be null) * @param context the current retry context - * @throws Exception if the callback does, and if there is no callback and - * the state is null then the last exception from the context - * @throws ExhaustedRetryException if the state is not null and there is no - * recovery callback + * @throws Exception if the callback does, and if there is no callback and the state + * is null then the last exception from the context + * @throws ExhaustedRetryException if the state is not null and there is no recovery + * callback */ - protected T handleRetryExhausted(RecoveryCallback recoveryCallback, RetryContext context, RetryState state) - throws Exception { + protected T handleRetryExhausted(RecoveryCallback recoveryCallback, + RetryContext context, RetryState state) throws Throwable { if (state != null) { retryContextCache.remove(state.getKey()); } @@ -436,28 +446,34 @@ public class RetryTemplate implements RetryOperations { } if (state != null) { logger.debug("Retry exhausted after last attempt with no recovery path."); - throw new ExhaustedRetryException("Retry exhausted after last attempt with no recovery path", context - .getLastThrowable()); + rethrow(context, "Retry exhausted after last attempt with no recovery path"); } throw wrapIfNecessary(context.getLastThrowable()); } + protected void rethrow(RetryContext context, String message) throws Throwable { + if (throwLastExceptionOnExhausted) { + throw (Throwable) context.getLastThrowable(); + } else { + throw new ExhaustedRetryException(message, context.getLastThrowable()); + } + } + /** - * Extension point for subclasses to decide on behaviour after catching an - * exception in a {@link RetryCallback}. Normal stateless behaviour is not - * to rethrow, and if there is state we rethrow. + * Extension point for subclasses to decide on behaviour after catching an exception + * in a {@link RetryCallback}. Normal stateless behaviour is not to rethrow, and if + * there is state we rethrow. * * @param retryPolicy * @param context the current context * - * @return true if the state is not null but subclasses might choose - * otherwise + * @return true if the state is not null but subclasses might choose otherwise */ - protected boolean shouldRethrow(RetryPolicy retryPolicy, RetryContext context, RetryState state) { + protected boolean shouldRethrow(RetryPolicy retryPolicy, RetryContext context, + RetryState state) { if (state == null) { return false; - } - else { + } else { return state.rollbackFor(context.getLastThrowable()); } } @@ -474,30 +490,30 @@ public class RetryTemplate implements RetryOperations { } - private void doCloseInterceptors(RetryCallback callback, RetryContext context, Throwable lastException) { + private void doCloseInterceptors(RetryCallback callback, RetryContext context, + Throwable lastException) { for (int i = listeners.length; i-- > 0;) { listeners[i].close(context, callback, lastException); } } - private void doOnErrorInterceptors(RetryCallback callback, RetryContext context, Throwable throwable) { + private void doOnErrorInterceptors(RetryCallback callback, + RetryContext context, Throwable throwable) { for (int i = listeners.length; i-- > 0;) { listeners[i].onError(context, callback, throwable); } } /** - * Re-throws the original throwable if it is unchecked, wraps checked - * exceptions into {@link RetryException}. + * Re-throws the original throwable if it is unchecked, wraps checked exceptions into + * {@link RetryException}. */ private static Exception wrapIfNecessary(Throwable throwable) { if (throwable instanceof Error) { throw (Error) throwable; - } - else if (throwable instanceof Exception) { + } else if (throwable instanceof Exception) { return (Exception) throwable; - } - else { + } else { return new RetryException("Exception in batch process", throwable); } } diff --git a/src/test/java/org/springframework/retry/config/EnableRetryTests.java b/src/test/java/org/springframework/retry/config/EnableRetryTests.java index c69bb13..b3b625a 100644 --- a/src/test/java/org/springframework/retry/config/EnableRetryTests.java +++ b/src/test/java/org/springframework/retry/config/EnableRetryTests.java @@ -24,8 +24,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; -import org.springframework.retry.config.EnableRetry; -import org.springframework.retry.config.Retryable; +import org.springframework.retry.backoff.Sleeper; /** * @author Dave Syer @@ -88,6 +87,15 @@ public class EnableRetryTests { @EnableAspectJAutoProxy(proxyTargetClass = true) protected static class TestConfiguration { + @Bean + public Sleeper sleper() { + return new Sleeper() { + @Override + public void sleep(long period) throws InterruptedException { + } + }; + } + @Bean public Service service() { return new Service(); diff --git a/src/test/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptorTests.java b/src/test/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptorTests.java index 6822e88..4f8f526 100644 --- a/src/test/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptorTests.java +++ b/src/test/java/org/springframework/retry/interceptor/StatefulRetryOperationsInterceptorTests.java @@ -174,7 +174,7 @@ public class StatefulRetryOperationsInterceptorTests { catch (ExhaustedRetryException e) { // expected String message = e.getMessage(); - assertTrue("Wrong message: " + message, message.startsWith("Retry was exhausted but there was no recover")); + assertTrue("Wrong message: " + message, message.startsWith("Retry exhausted")); } assertEquals(1, count); } diff --git a/src/test/java/org/springframework/retry/listener/RetryListenerTests.java b/src/test/java/org/springframework/retry/listener/RetryListenerTests.java index e17ffb2..876e4e1 100644 --- a/src/test/java/org/springframework/retry/listener/RetryListenerTests.java +++ b/src/test/java/org/springframework/retry/listener/RetryListenerTests.java @@ -40,7 +40,7 @@ public class RetryListenerTests { List list = new ArrayList(); @Test - public void testOpenInterceptors() throws Exception { + public void testOpenInterceptors() throws Throwable { template.setListeners(new RetryListener[] { new RetryListenerSupport() { public boolean open(RetryContext context, RetryCallback callback) { count++; @@ -65,7 +65,7 @@ public class RetryListenerTests { } @Test - public void testOpenCanVetoRetry() throws Exception { + public void testOpenCanVetoRetry() throws Throwable { template.registerListener(new RetryListenerSupport() { public boolean open(RetryContext context, RetryCallback callback) { list.add("1"); @@ -90,7 +90,7 @@ public class RetryListenerTests { } @Test - public void testCloseInterceptors() throws Exception { + public void testCloseInterceptors() throws Throwable { template.setListeners(new RetryListener[] { new RetryListenerSupport() { public void close(RetryContext context, RetryCallback callback, Throwable t) { count++; @@ -114,7 +114,7 @@ public class RetryListenerTests { } @Test - public void testOnError() throws Exception { + public void testOnError() throws Throwable { template.setRetryPolicy(new NeverRetryPolicy()); template.setListeners(new RetryListener[] { new RetryListenerSupport() { public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { @@ -146,7 +146,7 @@ public class RetryListenerTests { } @Test - public void testCloseInterceptorsAfterRetry() throws Exception { + public void testCloseInterceptorsAfterRetry() throws Throwable { template.registerListener(new RetryListenerSupport() { public void close(RetryContext context, RetryCallback callback, Throwable t) { list.add("" + count); diff --git a/src/test/java/org/springframework/retry/policy/FatalExceptionRetryPolicyTests.java b/src/test/java/org/springframework/retry/policy/FatalExceptionRetryPolicyTests.java index ac9b85c..3f92fdf 100644 --- a/src/test/java/org/springframework/retry/policy/FatalExceptionRetryPolicyTests.java +++ b/src/test/java/org/springframework/retry/policy/FatalExceptionRetryPolicyTests.java @@ -32,7 +32,7 @@ import org.springframework.retry.support.RetryTemplate; public class FatalExceptionRetryPolicyTests { @Test - public void testFatalExceptionWithoutState() throws Exception { + public void testFatalExceptionWithoutState() throws Throwable { MockRetryCallback callback = new MockRetryCallback(); callback.setExceptionToThrow(new IllegalArgumentException()); @@ -66,7 +66,7 @@ public class FatalExceptionRetryPolicyTests { } @Test - public void testFatalExceptionWithState() throws Exception { + public void testFatalExceptionWithState() throws Throwable { MockRetryCallback callback = new MockRetryCallback(); callback.setExceptionToThrow(new IllegalArgumentException()); diff --git a/src/test/java/org/springframework/retry/policy/StatefulRetryIntegrationTests.java b/src/test/java/org/springframework/retry/policy/StatefulRetryIntegrationTests.java index a1aac0d..42a135a 100644 --- a/src/test/java/org/springframework/retry/policy/StatefulRetryIntegrationTests.java +++ b/src/test/java/org/springframework/retry/policy/StatefulRetryIntegrationTests.java @@ -44,7 +44,7 @@ import org.springframework.retry.support.RetryTemplate; public class StatefulRetryIntegrationTests { @Test - public void testExternalRetryWithFailAndNoRetry() throws Exception { + public void testExternalRetryWithFailAndNoRetry() throws Throwable { MockRetryCallback callback = new MockRetryCallback(); RetryState retryState = new DefaultRetryState("foo"); @@ -86,7 +86,7 @@ public class StatefulRetryIntegrationTests { } @Test - public void testExternalRetryWithSuccessOnRetry() throws Exception { + public void testExternalRetryWithSuccessOnRetry() throws Throwable { MockRetryCallback callback = new MockRetryCallback(); RetryState retryState = new DefaultRetryState("foo"); @@ -120,7 +120,7 @@ public class StatefulRetryIntegrationTests { } @Test - public void testExponentialBackOffIsExponential() throws Exception { + public void testExponentialBackOffIsExponential() throws Throwable { ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy(); policy.setInitialInterval(100); policy.setMultiplier(1.5); diff --git a/src/test/java/org/springframework/retry/support/RetrySynchronizationManagerTests.java b/src/test/java/org/springframework/retry/support/RetrySynchronizationManagerTests.java index 0be7bfc..1b33597 100644 --- a/src/test/java/org/springframework/retry/support/RetrySynchronizationManagerTests.java +++ b/src/test/java/org/springframework/retry/support/RetrySynchronizationManagerTests.java @@ -42,7 +42,7 @@ public class RetrySynchronizationManagerTests { } @Test - public void testStatusIsStoredByTemplate() throws Exception { + public void testStatusIsStoredByTemplate() throws Throwable { RetryContext status = RetrySynchronizationManager.getContext(); assertNull(status); diff --git a/src/test/java/org/springframework/retry/support/RetryTemplateTests.java b/src/test/java/org/springframework/retry/support/RetryTemplateTests.java index 9ad9463..0a5ae49 100644 --- a/src/test/java/org/springframework/retry/support/RetryTemplateTests.java +++ b/src/test/java/org/springframework/retry/support/RetryTemplateTests.java @@ -54,7 +54,7 @@ public class RetryTemplateTests { int count = 0; @Test - public void testSuccessfulRetry() throws Exception { + public void testSuccessfulRetry() throws Throwable { for (int x = 1; x <= 10; x++) { MockRetryCallback callback = new MockRetryCallback(); callback.setAttemptsBeforeSuccess(x); @@ -67,7 +67,7 @@ public class RetryTemplateTests { } @Test - public void testSuccessfulRecovery() throws Exception { + public void testSuccessfulRecovery() throws Throwable { MockRetryCallback callback = new MockRetryCallback(); callback.setAttemptsBeforeSuccess(3); RetryTemplate retryTemplate = new RetryTemplate(); @@ -84,7 +84,7 @@ public class RetryTemplateTests { } @Test - public void testAlwaysTryAtLeastOnce() throws Exception { + public void testAlwaysTryAtLeastOnce() throws Throwable { MockRetryCallback callback = new MockRetryCallback(); RetryTemplate retryTemplate = new RetryTemplate(); retryTemplate.setRetryPolicy(new NeverRetryPolicy()); @@ -93,7 +93,7 @@ public class RetryTemplateTests { } @Test - public void testNoSuccessRetry() throws Exception { + public void testNoSuccessRetry() throws Throwable { MockRetryCallback callback = new MockRetryCallback(); // Something that won't be thrown by JUnit... callback.setExceptionToThrow(new IllegalArgumentException()); @@ -115,7 +115,7 @@ public class RetryTemplateTests { } @Test - public void testDefaultConfigWithExceptionSubclass() throws Exception { + public void testDefaultConfigWithExceptionSubclass() throws Throwable { MockRetryCallback callback = new MockRetryCallback(); int attempts = 3; callback.setAttemptsBeforeSuccess(attempts); @@ -129,7 +129,7 @@ public class RetryTemplateTests { } @Test - public void testRollbackClassifierOverridesRetryPolicy() throws Exception { + public void testRollbackClassifierOverridesRetryPolicy() throws Throwable { MockRetryCallback callback = new MockRetryCallback(); int attempts = 3; callback.setAttemptsBeforeSuccess(attempts); @@ -145,7 +145,7 @@ public class RetryTemplateTests { } @Test - public void testSetExceptions() throws Exception { + public void testSetExceptions() throws Throwable { RetryTemplate template = new RetryTemplate(); SimpleRetryPolicy policy = new SimpleRetryPolicy(3, Collections ., Boolean> singletonMap(RuntimeException.class, true)); @@ -170,7 +170,7 @@ public class RetryTemplateTests { } @Test - public void testBackOffInvoked() throws Exception { + public void testBackOffInvoked() throws Throwable { for (int x = 1; x <= 10; x++) { MockRetryCallback callback = new MockRetryCallback(); MockBackOffStrategy backOff = new MockBackOffStrategy(); @@ -187,7 +187,7 @@ public class RetryTemplateTests { } @Test - public void testEarlyTermination() throws Exception { + public void testEarlyTermination() throws Throwable { try { RetryTemplate retryTemplate = new RetryTemplate(); retryTemplate.execute(new RetryCallback() { @@ -206,15 +206,35 @@ public class RetryTemplateTests { } @Test - public void testNestedContexts() throws Exception { + public void testEarlyTerminationWithOriginalException() throws Throwable { + try { + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setThrowLastExceptionOnExhausted(true); + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Exception { + status.setExhaustedOnly(); + throw new IllegalStateException("Retry this operation"); + } + }); + fail("Expected ExhaustedRetryException"); + } + catch (IllegalStateException ex) { + // Expected for internal retry policy (external would recover + // gracefully) + assertEquals("Retry this operation", ex.getMessage()); + } + } + + @Test + public void testNestedContexts() throws Throwable { RetryTemplate outer = new RetryTemplate(); final RetryTemplate inner = new RetryTemplate(); outer.execute(new RetryCallback() { - public Object doWithRetry(RetryContext status) throws Exception { + public Object doWithRetry(RetryContext status) throws Throwable { context = status; count++; Object result = inner.execute(new RetryCallback() { - public Object doWithRetry(RetryContext status) throws Exception { + public Object doWithRetry(RetryContext status) throws Throwable { count++; assertNotNull(context); assertNotSame(status, context); @@ -231,7 +251,7 @@ public class RetryTemplateTests { } @Test - public void testRethrowError() throws Exception { + public void testRethrowError() throws Throwable { RetryTemplate retryTemplate = new RetryTemplate(); retryTemplate.setRetryPolicy(new NeverRetryPolicy()); try { @@ -248,7 +268,7 @@ public class RetryTemplateTests { } @Test - public void testFailedPolicy() throws Exception { + public void testFailedPolicy() throws Throwable { RetryTemplate retryTemplate = new RetryTemplate(); retryTemplate.setRetryPolicy(new NeverRetryPolicy() { @Override @@ -270,7 +290,7 @@ public class RetryTemplateTests { } @Test - public void testBackOffInterrupted() throws Exception { + public void testBackOffInterrupted() throws Throwable { RetryTemplate retryTemplate = new RetryTemplate(); retryTemplate.setBackOffPolicy(new StatelessBackOffPolicy() { protected void doBackOff() throws BackOffInterruptedException { @@ -295,7 +315,7 @@ public class RetryTemplateTests { * re-thrown. */ @Test - public void testNoBackOffForRethrownException() throws Exception { + public void testNoBackOffForRethrownException() throws Throwable { RetryTemplate tested = new RetryTemplate(); tested.setRetryPolicy(new SimpleRetryPolicy(1, Collections., Boolean> singletonMap( diff --git a/src/test/java/org/springframework/retry/support/StatefulRecoveryRetryTests.java b/src/test/java/org/springframework/retry/support/StatefulRecoveryRetryTests.java index 65ee145..b3b4afd 100644 --- a/src/test/java/org/springframework/retry/support/StatefulRecoveryRetryTests.java +++ b/src/test/java/org/springframework/retry/support/StatefulRecoveryRetryTests.java @@ -81,7 +81,7 @@ public class StatefulRecoveryRetryTests { } @Test - public void testRecover() throws Exception { + public void testRecover() throws Throwable { retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1, Collections ., Boolean> singletonMap(Exception.class, true))); final String input = "foo"; @@ -114,7 +114,7 @@ public class StatefulRecoveryRetryTests { } @Test - public void testSwitchToStatelessForNoRollback() throws Exception { + public void testSwitchToStatelessForNoRollback() throws Throwable { retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1, Collections ., Boolean> singletonMap(Exception.class, true))); // Roll back for these: @@ -145,7 +145,7 @@ public class StatefulRecoveryRetryTests { } @Test - public void testExhaustedClearsHistoryAfterLastAttempt() throws Exception { + public void testExhaustedClearsHistoryAfterLastAttempt() throws Throwable { RetryPolicy retryPolicy = new SimpleRetryPolicy(1, Collections ., Boolean> singletonMap(Exception.class, true)); retryTemplate.setRetryPolicy(retryPolicy); @@ -225,7 +225,7 @@ public class StatefulRecoveryRetryTests { } @Test - public void testCacheCapacity() throws Exception { + public void testCacheCapacity() throws Throwable { retryTemplate.setRetryPolicy(new SimpleRetryPolicy(1, Collections ., Boolean> singletonMap(Exception.class, true))); @@ -257,7 +257,7 @@ public class StatefulRecoveryRetryTests { } @Test - public void testCacheCapacityNotReachedIfRecovered() throws Exception { + public void testCacheCapacityNotReachedIfRecovered() throws Throwable { SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(1, Collections ., Boolean> singletonMap(Exception.class, true));