Can we add specific condition in #Retryable for any exception in spring-retry? - spring-boot

I have a class which has #Retryable annotation added to method with value as custom exception and maxAttempts =2 .
#Override
#Retryable(value = CustomException.class, maxAttempts = 2)
public void process(String input) {
//code logic
}
Currently this code is retried everytime there is a CustomException thrown in application but my code throws this CustomException in different ways like :
throw new CustomException(CustomErrorCode.RETRY)
throw new CustomException(CustomErrorCode.DONOTRETRY)
I want to retry CustomException which has errorcode Retry.
Can anybody help?

You cannot add conditions based on the exception properties; however, in the retryable method, you can do this:
RetrySynchronizationManager.getContext().setExhaustedOnly();
This prevents any retries.
/**
* Signal to the framework that no more attempts should be made to try or retry the
* current {#link RetryCallback}.
*/
void setExhaustedOnly();

Related

Is this how spring #Retryable supposed to work?

I recently added spring #Retryable to handle 502 errors instead of appropriate responses from the target server. (with retrofit2).
Below is just pseudo-code but the original code handles exceptions in a similar way.
class BadGatewayException : RuntimeException
#Retryable(include = [BadGatewayException::class])
class A {
private fun handle(block: () -> Call<T>): T {
try {
val response = val response = block().execute()
...
if (response.code() == 502) {
throw BadGatewayException("The server suffers temporary connection problems.")
}
...
} catch(e: Exception) {
throw RuntimeException("a system error has occurred")
}
}
}
I expected #Retryable wouldn't retry as the BadGatewayException that occurs with 502 would be wrapped in RuntimeException straight away in a catch block then thrown. But, when it was tested, it seemed like it follows this step
try to get a response from a retrofit request
502 occurs
BadGatewayException thrown
retry (3 by default) - here BadGatewayException is caught somehow
RuntimeException thrown
The point is, is #Retryable supposed to intercept any exceptions this way? Or am I missing something here?
In your pseudo-code you don't wrap your BadGatewayException into RuntimeException. But if you do something like throw RuntimeException("your text", e) where e is BadGatewayException I can explain what happens:
The class AnnotationAwareRetryOperationsInterceptor.java which works with annotation #Retryable creates RetryPolicy like return new SimpleRetryPolicy(maxAttempts, policyMap, true, retryNotExcluded);
The third parameter is traverseCauses and it is always true
It means that Retryable works with causes: when it gets RuntimeException it takes a cause which is BadGatewayException. This is why retry happens in your case if you do a wrap.
I dont know how change this behavior. In the latest version (2.0.0) there is still the same code of creation a retry policy.
I know it is late but maybe it will be helpful for someone
The SimpleRetryPolicy used by the annotation does not traverse the cause chain to look for classified exceptions; only the top level exception is compared to the classified list.
You would have to wire up your own RetryInterceptor with an appropriately configured RetryTemplate and SimpleRetryPolicy.
/**
* Create a builder for a stateless retry interceptor.
* #return The interceptor builder.
*/
public static StatelessRetryInterceptorBuilder stateless() {
return new StatelessRetryInterceptorBuilder();
}
RetryInterceptorBuilder.stateless()
.retryPolicy(new SimpleRetryPolicy(...))
.build();
See
/**
* Create a {#link SimpleRetryPolicy} with the specified number of retry attempts. If
* traverseCauses is true, the exception causes will be traversed until a match or the
* root cause is found. The default value indicates whether to retry or not for
* exceptions (or super classes thereof) that 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 true to traverse the exception cause chain until a classified
* exception is found or the root cause is reached.
* #param defaultValue the default action.
*/
public SimpleRetryPolicy(int maxAttempts, Map<Class<? extends Throwable>, Boolean> retryableExceptions,
boolean traverseCauses, boolean defaultValue) {
(defaultValue = true and Map.of(BadGatewayException.class, false)).
Set the bean name of the interceptor in the interceptor annotation property.

In Spring RabbitMQ I throw AmqpRejectAndDontRequeueException but message still requeue

My service listens to RabbitMQ queue. I configure retry policy in consumer side. When I throw exception, all dead-letter messages requeue. But depend on my business logic, after throwing StopRequeueException (every exception except SmsException) I want to stop retry for this message. But the message still requeue.
Here is my configuration
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true
initial-interval: 3s
max-attempts: 10
max-interval: 12s
multiplier: 2
missing-queues-fatal: false
if (!checkMobileService.isMobileNumberAdmitted(mobileNumber())) {
throw new StopRequeueException("SMS_BIMTEK.MOBILE_NUMBER_IS_NOT_ADMITTED");
}
My error handler:
public class CustomErrorHandler implements ErrorHandler {
#Override
public void handleError(Throwable t) {
if (!(t.getCause() instanceof SmsException)) {
throw new AmqpRejectAndDontRequeueException("Error Handler converted exception to fatal", t);
}
}
}
Calling the error handler is outside the scope of retry; it is called after retries are exhausted.
You need to classify which exceptions are retryable at the retry level and do the conversion in the recoverer.
Here is an example:
#SpringBootApplication
public class So67406799Application {
public static void main(String[] args) {
SpringApplication.run(So67406799Application.class, args);
}
#Bean
public RabbitRetryTemplateCustomizer customizer(
#Value("${spring.rabbitmq.listener.simple.retry.max-attempts}") int attempts) {
return (target, template) -> template.setRetryPolicy(new SimpleRetryPolicy(attempts,
Map.of(StopRequeueException.class, false), true, true));
}
#Bean
MessageRecoverer recoverer() {
return (msg, cause) -> {
throw new AmqpRejectAndDontRequeueException("Stop requeue after " +
RetrySynchronizationManager.getContext().getRetryCount() + " attempts");
};
}
#RabbitListener(queues = "so67406799")
void listen(String in) {
System.out.println(in);
if (in.equals("dontRetry")) {
throw new StopRequeueException("test");
}
throw new RuntimeException("test");
}
#Bean
Queue queue() {
return new Queue("so67406799");
}
}
#SuppressWarnings("serial")
class StopRequeueException extends NestedRuntimeException {
public StopRequeueException(String msg) {
super(msg);
}
}
EDIT
The customizer is called once by Spring Boot; it is called after the retry policy and back off policy have been set up. See RetryTemplateFactory.
In this case, the customizer replaces the retry policy with a new one with an exception classifier (that's why we need the max attempts injected here).
See the SimpleRetryPolicy constructor.
/**
* 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 true to traverse the exception cause chain until a classified
* exception is found or the root cause is reached.
* #param defaultValue the default action.
*/
public SimpleRetryPolicy(int maxAttempts, Map<Class<? extends Throwable>, Boolean> retryableExceptions,
boolean traverseCauses, boolean defaultValue) {
The last boolean in the config above (true) is the default behavior (retry exceptions that are not in the map), the third (true) tells the policy to follow the cause chain to look for the exception (like your getCause() in the error handler). The map <Exception, Boolean> says don't retry for this one.
You can also configure it the other way around (default false and true in the map values), explicitly stating which exceptions you want to retry and don't for all others.
The MessageRecoverer is called for all exceptions, either immediately for the classified exception or when retries are exhausted for the others.

Spring Boot - how to call another method outside of the transaction scope

I have the following method:
#Transactional
public Store handle(Command command) {
Store store= mapper.map(command.getStoreDto(), Store.class);
Store persistedStore = storeService.save(store);
addressService.saveStoreAddress(store, command.getEmployeeId()); //this method is not crucial, should be called independently and in another transaction, without any rollback in case of exception
return persistedStore;
}
addressService.saveStoreAddress is not crucial - when this method will throw any exception, store should be saved anyway (storeService.save(store);). What is the best solution in my case?
Use #Transactional(propagation=REQUIRES_NEW) on the saveStoreAddress() such that it will execute in a new and separate transaction.
To prevent the transaction of the handle() will be rollback because of the exception throw from saveStoreAddress() , you also have to try-catch when calling saveStoreAddress().
In the end , it looks something like:
#Service
public class AddressService {
#Transactional(propagation=REQUIRES_NEW)
public void saveStoreAdress(.....){
}
}
#Transactional
public Store handle(Command command) {
.......
try{
addressService.saveStoreAddress(store, command.getEmployeeId());
}catch (Exception ex){
/***
* handle the exception thrown from saveStoreAddress.
* If you want the current transaction not rollback just because of the
* exception throw from saveStoreAddress(), do not re-throw the exception when
* handling this exception
*/
}
return ....;
}

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");
}
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()));

Overriding errorChannel configured in #MessagingGateway

I have configured #MessagingGateway as below to use an error channel, which works as expected.
#MessagingGateway(errorChannel = "DefaultInboundErrorHandlerChannel")
public interface InboundMessagingGateway {
#Gateway(requestChannel = "InboundEntryChannel")
void receive(XferRes response);
}
Within the flow I am passing the object to a transformer as below:
Step 1:
#Transformer(inputChannel = "InboundEntryChannel", outputChannel = "TransmissionLogChannel")
public CassandraEntity createEntity(
org.springframework.messaging.Message<XferRes> message) throws ParseException {
XferRes response = message.getPayload();
CassandraEntity entity = new CassandraEntity();
// ... getters & setter ommitted for brevity
return entity;
}
Next, I update the entity as below:
Step 2:
#ServiceActivator(inputChannel = "TransmissionLogChannel", outputChannel="PublishChannel")
public XferRes updateCassandraEntity(
org.springframework.messaging.Message<XferRes> message) {
XferRes response = message.getPayload();
this.cassandraServiceImpl.update(response);
return response;
}
And last, I post to a Kafka topic as below:
Step 3:
#ServiceActivator(inputChannel = "PublishChannel")
public void publish(org.springframework.messaging.Message<XferRes> message){
XferRes response = message.getPayload();
publisher.post(response);
}
In case of an error I post the message to a service which publishes the error object to log ingestion:
#ServiceActivator(inputChannel="defaultInboundErrorHandlerChannel")
public void handleInvalidRequest(org.springframework.messaging.Message<MessageHandlingException> message) throws ParseException {
XferRes originalRequest = (XferRes) message.getPayload().getFailedMessage().getPayload();
this.postToErrorBoard(originalRequest)
}
If an error occurs at Step 2: in updating the DB, then also I want to invoke Step 3. A trivial way is to remove the Step 2 & make the call to update database from Step 1.
Is there any other way in Spring Integration where I can invoke Step 3 irrespective if an error occurs or not.
This technique called PublishSubscribeChannel. Since I see that you reuse a payload on the second step to send to the third step, then it is definitely a use-case for the PublishSubscribeChannel and two sequential subscribers to it.
I mean you create a PublishSubscribeChannel #Bean and those #ServiceActivators are use the name to this channel.
More info is in the Reference Manual. Pay attention to the ignoreFailures property:
/**
* Specify whether failures for one or more of the handlers should be
* ignored. By default this is <code>false</code> meaning that an Exception
* will be thrown whenever a handler fails. To override this and suppress
* Exceptions, set the value to <code>true</code>.
* #param ignoreFailures true if failures should be ignored.
*/
public void setIgnoreFailures(boolean ignoreFailures) {

Resources