JohnD
JohnD

Reputation: 465

How to wrap exception on exhausted retries with Spring Retry

Context:

I'm using spring-retry to retry restTemplate calls.

The restTemplate calls are called from a kafka listener. The kafka listener is also configured to retry on error (if any exception are thrown during the process, not only the restTemplate call).

Goal:

I'd like to prevent kafka from retrying when the error come from a retry template which has exhausted.

Actual behavior :

When the retryTemplate exhaust all retries, the original exception is thrown. Thus preventing me from identifying if the error was retried by the retryTemplate.

Desired behavior:

When the retryTemplate exhaust all retries, wrap the original exception in a RetryExhaustedException which will allow me to blacklist it from kafka retries.

Question:

How can I do something like this ?

Thanks

Edit

RetryTemplate configuration :

RetryTemplate retryTemplate = new RetryTemplate();

FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(1000);
retryTemplate.setBackOffPolicy(backOffPolicy);

Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>();
retryableExceptions.put(FunctionalException.class, false);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(3, retryableExceptions, true, true);
retryTemplate.setRetryPolicy(retryPolicy);

retryTemplate.setThrowLastExceptionOnExhausted(false);

Kafka ErrorHandler

public class DefaultErrorHandler implements ErrorHandler {

@Override
public void handle(Exception thrownException, ConsumerRecord<?, ?> data) {
    Throwable exception = Optional.ofNullable(thrownException.getCause()).orElse(thrownException);

    // TODO if exception as been retried in a RetryTemplate, stop it to prevent rollback and send it to a DLQ
    // else rethrow exception, it will be rollback and handled by AfterRollbackProcessor to be retried
    throw new KafkaException("Could not handle exception", thrownException);
   }
}

Listener kafka :

@KafkaListener
public void onMessage(ConsumerRecord<String, String> record) {
    retryTemplate.execute((args) -> {
        throw new RuntimeException("Should be catched by ErrorHandler to prevent rollback");
    }
    throw new RuntimeException("Should be retried by afterRollbackProcessor");
}

Upvotes: 2

Views: 9728

Answers (1)

Gary Russell
Gary Russell

Reputation: 174769

Simply configure the listener retry template with a SimplyRetryPolicy that is configured to classify RetryExhaustedException as not retryable.

Be sure to set the traverseCauses property to true since the container wraps all listener exceptions in ListenerExecutionFailedException.

/**
 * Create a {@link SimpleRetryPolicy} with the specified number of retry
 * attempts. If traverseCauses is true, the exception causes will be traversed until
 * a match is found. The default value indicates whether to retry or not for exceptions
 * (or super classes) are not found in the map.
 *
 * @param maxAttempts the maximum number of attempts
 * @param retryableExceptions the map of exceptions that are retryable based on the
 * map value (true/false).
 * @param traverseCauses is this clause traversable
 * @param defaultValue the default action.
 */
public SimpleRetryPolicy(int maxAttempts, Map<Class<? extends Throwable>, Boolean> retryableExceptions,
        boolean traverseCauses, boolean defaultValue) {

EDIT

Use

template.execute((args) -> {...}, (context) -> throw new Blah(context.getLastThrowable()));

Upvotes: 2

Related Questions