Spring Retry, because Winter is coming
OK, this is actually not about the winter, which as we all know has already come. It is about Spring Retry, a small Spring Framework library that allows us to add retry functionality to any task that should be retryable.
There’s a very good tutorial here, explaining how the simple retry and recovery is set up. It explains very well how to add a spring-retry dependency, use @Retryable and @Recover annotation and use RetryTemplate with simple policies. What I’d like to linger on is a slightly more complicated case when we actually want to apply different retry behavior based on the type of the exception. This makes sense because we might know that some exceptions are recoverable and some are not, and therefore it doesn’t make too much sense to try and recover from them. For that, there is a specific retry strategy implementation which is called ExceptionClassifierRetryPolicy, which is used with the Spring RetryTemplate.
Let’s suppose we can only recover from IO Exceptions and skip all others. We will create three classes to extend RetryCallback and one class to extend RecoveryCallback to better show what happens inside:
private class SuccessCallback implements RetryCallback<Boolean, RuntimeException> { @Override public Boolean doWithRetry(RetryContext context) throws RuntimeException { System.out.println("Success callback: attempt " + context.getRetryCount()); return true; } } private class ExceptionCallback implements RetryCallback<Boolean, Exception> { @Override public Boolean doWithRetry(RetryContext context) throws Exception { System.out.println("Exception callback: attempt " + context.getRetryCount()); throw new Exception("Test Exception"); } } private class SpecificExceptionCallback implements RetryCallback<Boolean, IOException> { @Override public Boolean doWithRetry(RetryContext context) throws IOException { System.out.println("IO Exception callback: attempt " + context.getRetryCount()); throw new IOException("Test IO Exception"); } } private class LoggingRecoveryCallback implements RecoveryCallback<Boolean> { @Override public Boolean recover(RetryContext context) throws Exception { System.out.println("Attempts exhausted. Total: " + context.getRetryCount()); System.out.println("Last exception: " + Optional.ofNullable(context.getLastThrowable()) .orElse(new Throwable("No exception thrown")).getMessage()); System.out.println("\n"); return false; } }
Then we set up our RetryTemplate. We’ll be using a SimpeRetryPolicy with the fixed number of attempts for the IOException and a NeverRetryPolicy which just allows the initial attempt for everything else.
* We want to retry on IOException only. Other Exceptions won't be retried. IOException will be retried three times, counting the initial attempt. */ final ExceptionClassifierRetryPolicy exRetryPolicy = new ExceptionClassifierRetryPolicy(); exRetryPolicy.setPolicyMap(new HashMap<Class<? extends Throwable>, RetryPolicy>() {{ put(IOException.class, new SimpleRetryPolicy(3)); put(Exception.class, new NeverRetryPolicy()); }}); retryTemplate.setRetryPolicy(exRetryPolicy);
Now we need to use these callbacks to demonstrate how they work. First the successfull execution, which is very simple:
// we do not catch anything here System.out.println("\n*** Executing successfull callback..."); retryTemplate.execute(new SuccessCallback(), new LoggingRecoveryCallback());
The output for it is as follows:
*** Executing successfull callback... Success callback: attempt 0
Then the Exception:
// we catch Exception to allow the program to continue System.out.println("\n*** Executing Exception callback..."); try { retryTemplate.execute(new ExceptionCallback(), new LoggingRecoveryCallback()); } catch (Exception e) { System.out.println("Suppressed Exception"); }
*** Executing Exception callback... Exception callback: attempt 0 Attempts exhausted. Total: 1 Last exception: Test Exception
And at last our IOException:
// we catch IOException to allow the program to continue System.out.println("\n*** Executing IO Exception callback..."); try { retryTemplate.execute(new SpecificExceptionCallback(), new LoggingRecoveryCallback()); } catch (IOException e) { System.out.println("Suppressed IO Exception"); }
*** Executing IO Exception callback... IO Exception callback: attempt 0 IO Exception callback: attempt 1 IO Exception callback: attempt 2 Attempts exhausted. Total: 3 Last exception: Test IO Exception
As we can see, only IOException initiated three attempts. Note that the attempts are numbered from 0 because when the callback is executed the attempt is not exhausted, so the last attempt has #2 and not #3. But on RecoveryCallback all the attempts are exhausted, so the context holds 3 attempts.
We can also see that the RecoveryCallback isn’t called when the attempts were a success. That is, it is only called when the execution ended with an exception.
The RetryTemplate is synchronous, so all the execution happens in our main thread. That is why I added try/catch blocks around the calls, to allow the program run all three examples without a problem. Otherwise the retry policy would rethrow the exception after its last unsuccessful attempt and would stop the execution.
There is also a very interesting CompositeRetryPolicy which allows to add several policies and delegates to call them in order, one by one. It can also allow to create quite a flexible retry strategy, but that is another topic in itself.
I think that spring-retry is a very useful library which allows to make common retryable tasks more predictable, testable and easier to implement.
Reference: | Spring Retry, because Winter is coming from our JCG partner Maryna Cherniavska at the Software development and other animals blog. |
Really well explained….