How to trap error with a transactional annotation? - spring

I use spring boot 2.2.
In a method with transactional anocation, when I save via repository if there is no error, I want to send a message with rabbit mq.
How to be sure there is no error with repository?
#Transactional
public void save(CreditEvent creditEvent) {
repository.save(creditEvent);
//no error send message
}
if there is an error when sending message, I don't want to rollback saving operation.

Although it's Transactional and JPA, still it's a java method which if save failed then unchecked DataAccessException exception will be thrown and flow won't continue to send message.
class is a runtime exception, there is no need for user code to catch it or subclasses if any error is to be considered fatal (the usual case).

#Transactional
public void save(CreditEvent creditEvent) {
try {
repository.save(creditEvent);
//no error send message}
catch {
// send message
// rethrow error
}
}

Related

How to simply requeue a RabbitMQ message in case of code exception with SpringBoot

I would like to learn an easy way to requeue a RabbitMQ if an exception is thrown in an SpringBoot application.
#RabbitListener(queues = TRANSACTION_171_REQUEST_QUEUE, errorHandler = "receiverExceptionHandler" )
public void listen171RequestsQueue(Transaction171Request request) {
try {
Transaction171Response response = null;
send171Response("OK", request.getNumeroFormularioRenach());
} catch (Exception e){
//Requeue message
}
}
My code behaviour is to consume a message and take it out of the queue independing of what it happens. I would like to requeue message in RabbitMQ if an exception is thrown.
Could you help me?
I am working in a SpringBoot application with Java 11.
RabbitMQ's default behavior is to requeue when there is an unhandled exception. In your case, you could remove the try..catch block or in the catch block just re-throw the exception.

Message are not commited (loss) when using #TransactionalEventListener to send a message in a JPA Transaction

Background of the code:
In order to replicate a production scenario, I have created a dummy app that will basically save something in DB in a transaction, and in the same method, it publishEvent and publishEvent send a message to rabbitMQ.
Classes and usages
Transaction Starts from this method.:
#Override
#Transactional
public EmpDTO createEmployeeInTrans(EmpDTO empDto) {
return createEmployee(empDto);
}
This method saves the record in DB and also triggers publishEvent
#Override
public EmpDTO createEmployee(EmpDTO empDTO) {
EmpEntity empEntity = new EmpEntity();
BeanUtils.copyProperties(empDTO, empEntity);
System.out.println("<< In Transaction : "+TransactionSynchronizationManager.getCurrentTransactionName()+" >> Saving data for employee " + empDTO.getEmpCode());
// Record data into a database
empEntity = empRepository.save(empEntity);
// Sending event , this will send the message.
eventPublisher.publishEvent(new ActivityEvent(empDTO));
return createResponse(empDTO, empEntity);
}
This is ActivityEvent
import org.springframework.context.ApplicationEvent;
import com.kuldeep.rabbitMQProducer.dto.EmpDTO;
public class ActivityEvent extends ApplicationEvent {
public ActivityEvent(EmpDTO source) {
super(source);
}
}
And this is TransactionalEventListener for the above Event.
//#Transactional(propagation = Propagation.REQUIRES_NEW)
#TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onActivitySave(ActivityEvent activityEvent) {
System.out.println("Activity got event ... Sending message .. ");
kRabbitTemplate.convertAndSend(exchange, routingkey, empDTO);
}
This is kRabbitTemplate is a bean config like this :
#Bean
public RabbitTemplate kRabbitTemplate(ConnectionFactory connectionFactory) {
final RabbitTemplate kRabbitTemplate = new RabbitTemplate(connectionFactory);
kRabbitTemplate.setChannelTransacted(true);
kRabbitTemplate.setMessageConverter(kJsonMessageConverter());
return kRabbitTemplate;
}
Problem Definition
When I am saving a record and sending a message on rabbitMQ using the above code flow, My messages are not delivered on the server means they lost.
What I understand about the transaction in AMQP is :
If the template is transacted, but convertAndSend is not called from Spring/JPA Transaction then messages are committed within the template's convertAndSend method.
// this is a snippet from org.springframework.amqp.rabbit.core.RabbitTemplate.doSend()
if (isChannelLocallyTransacted(channel)) {
// Transacted channel created by this template -> commit.
RabbitUtils.commitIfNecessary(channel);
}
But if the template is transacted and convertAndSend is called from Spring/JPA Transaction then this isChannelLocallyTransacted in doSend method will evaluate false and commit will be done in the method which initiated Spring/JPA Transaction.
What I found after investigating the reason for message loss in my above code.
Spring transaction was active when I called convertAndSend method, so it was supposed to commit the message in Spring transaction.
For that, RabbitTemplate binds the resources and registers the Synchronizations before sending the message in bindResourceToTransaction of org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils.
public static RabbitResourceHolder bindResourceToTransaction(RabbitResourceHolder resourceHolder,
ConnectionFactory connectionFactory, boolean synched) {
if (TransactionSynchronizationManager.hasResource(connectionFactory)
|| !TransactionSynchronizationManager.isActualTransactionActive() || !synched) {
return (RabbitResourceHolder) TransactionSynchronizationManager.getResource(connectionFactory); // NOSONAR never null
}
TransactionSynchronizationManager.bindResource(connectionFactory, resourceHolder);
resourceHolder.setSynchronizedWithTransaction(true);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new RabbitResourceSynchronization(resourceHolder,
connectionFactory));
}
return resourceHolder;
}
In my code, after resource bind, it is not able to registerSynchronization because TransactionSynchronizationManager.isSynchronizationActive()==false. and since it fails to registerSynchronization, spring commit did not happen for the rabbitMQ message as AbstractPlatformTransactionManager.triggerAfterCompletion calls RabbitMQ's commit for each synchronization.
What problem I faced because of the above issue.
Message was not committed in the spring transaction, so the message lost.
As resource was added in bindResourceToTransaction, this resource remained bind and did not let add the resource for any other message to send in the same thread.
Possible Root Cause of TransactionSynchronizationManager.isSynchronizationActive()==false
I found the method which starts the transaction removed the synchronization in triggerAfterCompletion of org.springframework.transaction.support.AbstractPlatformTransactionManager class. because status.isNewSynchronization() evaluated true after DB opertation (this usually not happens if I call convertAndSend without ApplicationEvent).
private void triggerAfterCompletion(DefaultTransactionStatus status, int completionStatus) {
if (status.isNewSynchronization()) {
List<TransactionSynchronization> synchronizations = TransactionSynchronizationManager.getSynchronizations();
TransactionSynchronizationManager.clearSynchronization();
if (!status.hasTransaction() || status.isNewTransaction()) {
if (status.isDebug()) {
logger.trace("Triggering afterCompletion synchronization");
}
// No transaction or new transaction for the current scope ->
// invoke the afterCompletion callbacks immediately
invokeAfterCompletion(synchronizations, completionStatus);
}
else if (!synchronizations.isEmpty()) {
// Existing transaction that we participate in, controlled outside
// of the scope of this Spring transaction manager -> try to register
// an afterCompletion callback with the existing (JTA) transaction.
registerAfterCompletionWithExistingTransaction(status.getTransaction(), synchronizations);
}
}
}
What I Did to overcome on this issue
I simply added #Transactional(propagation = Propagation.REQUIRES_NEW) along with on #TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) in onActivitySave method and it worked as a new transaction was started.
What I need to know
Why this status.isNewSynchronization in triggerAfterCompletion method when using ApplicationEvent?
If the transaction was supposed to terminate in the parent method, why I got TransactionSynchronizationManager.isActualTransactionActive()==true in Listner class?
If Actual Transaction Active, was it supposed to remove the synchronization?
In bindResourceToTransaction, do spring AMQP assumed an active transaction without synchronization? if the answer is yes, why not to synchronization. init if it is not activated?
If I am propagating a new transaction then I am losing the parent transaction, is there any better way to do it?
Please help me on this, it is a hot production issue, and I am not very sure about the fix I have done.
This is a bug; the RabbitMQ transaction code pre-dated the #TransactionalEventListener code, by many years.
The problem is, with this configuration, we are in a quasi-transactional state, while there is indeed a transaction in process, the synchronizations are already cleared because the transaction has already committed.
Using #TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) works.
I see you already raised an issue:
https://github.com/spring-projects/spring-amqp/issues/1309
In future, it's best to ask questions here, or raise an issue if you feel there is a bug. Don't do both.

Quarkus hibernate validations exceptions not showing on the console

I have a simple project using Quarkus 1.4.2. When I use the #Valid annotation, and the validations fail with a status 500, the exception is not show on the console. Only in the Swagger UI. What should I do to print it out on the console?
#ApplicationScoped
public class ProductService {
public void validateProduct(#Valid Product product) {
}
}
The exception that is occurring is:
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint
The error is correct. It is just not shown on the console.
I would expect the error to be logged as it's definitely a usability issue. And I would expect it to be logged on startup when we collect the Hibernate Validator metadata, not for every call.
You could create a reproducer and open a GitHub issue in the Quarkus tracker here.
I'll check it out and see if something needs fixing.
If I understand correctly, you need to use the Validator object in order to catch possible Exceptions:
#Inject
Validator validator;
public void validateProduct(Product product) {
// Should throw an error
Set<ConstraintViolation<Product>> violations = validator.validate(product);
if(violations.isEmpty()) {
return;
}
for (ConstraintViolation<Product> violation : violations) { // or log whole set as json
System.out.println(violation.toString()); //TODO prettify
}
throw new ValidationException(JsonbBuilder.create().toJson(violations));
}
If you get a 500 error, you can now catch it and log.
Or just catch UnexpectedTypeException where you call your service. This might be better.

Catching exception in Kafkalistener Integration test

So I need to create an integration test for my kafkalistener method, where the test expects ListenerExecutionFailedException is actually thrown because the message was failed during consumption due to another service being inactive.
Below is the test code, where I use embeddedkafkabroker for producer and consumer:
#Test(expected = ListenerExecutionFailedException.class)
public void shouldThrowException() {
RecordHeaders recordHeaders = new RecordHeaders();
recordHeaders.add(new RecordHeader("messageType", "bootstrap".getBytes()));
recordHeaders.add(new RecordHeader("userId", "123".getBytes()));
recordHeaders.add(new RecordHeader("applicationId", "1234".getBytes()));
recordHeaders.add(new RecordHeader("correlationId", UUID.randomUUID().toString().getBytes()));
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(
"TEST_TOPIC",
1,
null,
"message",
"",
recordHeaders);
producer.send(producerRecord);
consumer.subscribe(Collections.singleton("TEST_TOPIC"));
consumer.poll(Duration.ofSeconds(2));
}
What I'm wondering is the exception was considered as not thrown and the test fails even though I know the message is indeed received by the listener and the exception was thrown since I saw them on the log.
And even though I changed the expected into Throwable no exception seems to be detected.
What should I do to make the exception to be detected by Junit?
Also, another interesting thing is that I tried to mock the service class which was called in the listener and return some dummy value but the service is not called when I used Mockito.verify
You seem to have some misunderstanding.
producer.send
consumer.poll
You are calling the kafka-clients directly and are not using Spring at all in this test.
ListenerExecutionFailedException is an exception that Spring's listener container wraps user exceptions thrown by message listeners.

Using Spring AOP, How can I intercept a exception just when it occurred?

Some code like this:
public class A {
#Autoware
private B b;
public void a() {
//AAA: some logic process that maybe throw exception
b.b();
}
}
public class B {
public void b() {
//BBB: some logic process maybe also throw exception
}
}
Both exceptions in A.a() and B.b() need to be intercept, so i use #AfterThrowing annotation do it. but the question is, when i call A.a() in other code and exception has occurred in B.b(), the Advice will execute twice! because exception that occurred in B.b() was propagating to its caller A.a().
I can't swallow the exception silently, because i use spring-amqp, above codes is on Consumer side, i need some message processing that based on the exceptions that occurred in Consumer.
#Around does not work too since i can't swallow the throwed exception.
So, How can i intercept a exception just when it occurred? ignore propagation of it.
Any reply is greatly appreciated.

Resources