13

I am using Spring-Retry for some database operations. On a SQLRecoverableException I retry three times (this assumes that whatever is causing the exception is non-transient if it fails three times), on a SQLTransientException I retry indefinitely (the program can't do anything without access to the database, so it may as well keep retrying until the user decides to reboot the server), and on any other exception I don't retry. I use an exponential backoff policy with a base retry of 100ms and a max retry of 30,000ms.

private static final int MAX_RECOVERABLE_RETRIES = 3;
private static final long INITIAL_INTERVAL = 100;
private static final long MAX_INTERVAL = 30 * 1000;
private static final double MULTIPLIER = 2.0;

public static RetryTemplate databaseTemplate() {
    RetryTemplate template = new RetryTemplate();
    ExceptionClassifierRetryPolicy retryPolicy = new ExceptionClassifierRetryPolicy();
    Map<Class<? extends Throwable>, RetryPolicy> policyMap = new HashMap<>();
    NeverRetryPolicy baseException = new NeverRetryPolicy();
    SimpleRetryPolicy recoverablePolicy = new SimpleRetryPolicy();
    recoverablePolicy.setMaxAttempts(MAX_RECOVERABLE_RETRIES);
    AlwaysRetryPolicy transientPolicy = new AlwaysRetryPolicy();
    policyMap.put(Exception.class, baseException);
    policyMap.put(SQLRecoverableException.class, recoverablePolicy);
    policyMap.put(SQLTransientException.class, transientPolicy);
    retryPolicy.setPolicyMap(policyMap);
    template.setRetryPolicy(retryPolicy);
    ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
    backOffPolicy.setInitialInterval(INITIAL_INTERVAL);
    backOffPolicy.setMaxInterval(MAX_INTERVAL);
    backOffPolicy.setMultiplier(MULTIPLIER);
    template.setBackOffPolicy(backOffPolicy);
    return template;
}

Ideally, I would like to use a fixed backoff of 100ms for all SQLRecoverableExceptions, and only apply the exponential backoff policy to SQLTransientExceptions. I could accomplish this with nested retries, but that will greatly increase the code complexity - given no other option I would prefer to simply apply the exponential backoff to both SQLRecoverableException and SQLTransientException exceptions.

Is there a way for me to apply different backoff policies to different exceptions using a single retry template?

Zim-Zam O'Pootertoot
  • 17,888
  • 4
  • 41
  • 69

3 Answers3

7

Indeed, ExceptionClassifierRetryPolicy is the way to go. I didn't manage to make it work with the policyMap though.

Here is how I've used it:

@Component("yourRetryPolicy")
public class YourRetryPolicy extends ExceptionClassifierRetryPolicy
{
    @PostConstruct
    public void init()
    {
        final SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
        simpleRetryPolicy.setMaxAttempts( 3 );

        this.setExceptionClassifier( new Classifier<Throwable, RetryPolicy>()
        {
            @Override
            public RetryPolicy classify( Throwable classifiable )
            {
                    if ( classifiable instanceof YourException )
                    {
                            return new NeverRetryPolicy();
                    }
                    // etc...
                    return simpleRetryPolicy;
            }
        });
    }
}

Then, you just have to set it on the retry template :

@Autowired
@Qualifier("yourRetryPolicy")
private YourRetryPolicy yourRetryPolicy;

//...

RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy( yourRetryPolicy );
Pablo Fernandez
  • 279,434
  • 135
  • 377
  • 622
oailloud
  • 363
  • 1
  • 2
  • 7
  • 1
    This helps with choosing different `RetryPolicy`s, but the OP wanted to choose different `BackOffPolicy`s. @Zim-Zam O'Pootertoot Is this what you used? – asgs Dec 15 '18 at 21:53
3

The accepted answer only deals with exception-specific RetryPolicy instances. Spring doesn't provide any functionality out of the box for exception-specific BackOffPolicy instances. Luckily it's straightforward to implement.

import org.springframework.classify.Classifier
import org.springframework.classify.ClassifierSupport
import org.springframework.classify.SubclassClassifier
import org.springframework.retry.RetryContext
import org.springframework.retry.backoff.BackOffContext
import org.springframework.retry.backoff.BackOffInterruptedException
import org.springframework.retry.backoff.BackOffPolicy
import org.springframework.retry.backoff.NoBackOffPolicy

class ExceptionClassifierBackoffPolicy implements BackOffPolicy {

    private static class ExceptionClassifierBackoffContext implements BackOffContext, BackOffPolicy {
        Classifier<Throwable, BackOffPolicy> exceptionClassifier
        RetryContext retryContext
        Map<BackOffPolicy, BackOffContext> policyContextMap = [:]

        ExceptionClassifierBackoffContext(Classifier<Throwable, BackOffPolicy> exceptionClassifier, RetryContext retryContext) {
            this.exceptionClassifier = exceptionClassifier
            this.retryContext = retryContext
        }

        @Override
        BackOffContext start(RetryContext context) {
            return null
        }

        @Override
        void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
            def policy = exceptionClassifier.classify(retryContext.lastThrowable)
            def policyContext = policyContextMap.get(policy)
            if (!policyContext) {
                policyContext = policy.start(retryContext)
                policyContextMap.put(policy, policyContext)
            }
            policy.backOff(policyContext)
        }
    }
    private Classifier<Throwable, BackOffPolicy> exceptionClassifier = new ClassifierSupport<Throwable, BackOffPolicy>(new NoBackOffPolicy());

    void setPolicyMap(Map<Class<? extends Throwable>, BackOffPolicy> policyMap) {
        exceptionClassifier = new SubclassClassifier<Throwable, BackOffPolicy>(policyMap, new NoBackOffPolicy());
    }

    @Override
    BackOffContext start(RetryContext context) {
        return new ExceptionClassifierBackoffContext(exceptionClassifier, context)
    }

    @Override
    void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
        def classifierBackOffContext = (ExceptionClassifierBackoffContext) backOffContext
        classifierBackOffContext.backOff(backOffContext)
    }
}

Then just:

BackOffPolicy backOffPolicy = new ExceptionClassifierBackoffPolicy()
def policyMap = [
        (RuntimeException): new FixedBackOffPolicy(backOffPeriod: 1000),
        (IOException)     : new ExponentialRandomBackOffPolicy(initialInterval: 500, maxInterval: 360000, multiplier: 2)
] as Map<Class<? extends Throwable>, BackOffPolicy>
backOffPolicy.policyMap = backoffPolicyMap
Adam Augusta
  • 444
  • 4
  • 9
0

A little late to the party but when all you need is having different backoff periods depending on the type of the exception, extending FixedBackoffPolicy should do the trick and is pretty straightforward. Something along these lines:

First you create your backoff policy class that receives a map with different backoff periods per exception type:

public class MultipleExceptionsBackoffPolicy extends FixedBackoffPolicy 
{
    private Classifier<Throwable, Long> classifier;

    public MultipleExceptionsBackoffPolicy (final Map<Class<? extends Throwable>, Long> throwableBackoffMap) {
        classifier = new SubclassClassifier<>(throwableBackoffMap, 5_000L) // default is 5s
    }

    @Override
    protected void doBackOff() exception BackOffInterruptedException {
        final backoff = classifier.classify(RetrySynchronizationManager.getContext().getLastThrowable());
        setBackOffPeriod(backoff);
        super.doBackOff();
    }
}

then you have to create a custom interceptor such as:

@Bean
public Object myCustomInterceptor (){
    var exBackoffMap = Map.of(
        ExceptionTypeOne.class, 2_000L, // If ExceptionTypeOne happens, backoff period is 2s
        ExceptionTypeTwo.class, 7_000L // and if ExceptionTypeTwo happens, the backoff period is 7s
    )
    return RetryInterceptorBuilder
                .stateless()
                .retryPolicy(new SimpleRetryPolicy(3)) // always 3 attempts no matter what
                .backOffPolicy(new MultipleExceptionsBackoffPolicy(exBackoffMap))
                .build();
}

and finally, you just configure the custom interceptor in your retryable:

@Retryable(interceptor="myCustomInterceptor")
public @interface MyRetryable {}
Victor Caveda
  • 686
  • 1
  • 6
  • 6