Kafka transaction rollback not working with 3 topics for RecordTooLargeException - spring-boot

I post 3 message to 3 topics - while posting if gets exception - all messages will be rolled back.
But in my case it is not happening when I simulate the below exception for 3rd Topic.
org.apache.kafka.common.errors.RecordTooLargeException: The message is 117440606 bytes
while posting large message to the 3rd topic (price topic)- I programmatically increase the Size of the message to get exception.
Message are send to 1st 2 topic successfully - 3rd one failed. - As per transaction all messages must be rolled back - but topic 1 and 2 all the time gets the message.
But LOG shows - Transaction rolled back
HOW to FIX this issue
Log
2022-03-23 21:16:59.690 [org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1] INFO c.a.m.r.producer.KafkaProducer - ### --- Sending Data to Item , price, Inventory -----
2022-03-23 21:16:59.733 [org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1] ERROR o.s.k.s.LoggingProducerListener - Exception thrown when sending a message with key='String' and payload='{"sku":"String","lowestOriginalPrice":...' to topic PRICE-TOPIC:
**org.apache.kafka.common.errors.RecordTooLargeException**: The message is 117440606 bytes when serialized which is larger than 1048576, which is the value of the max.request.size configuration.
2022-03-23 21:16:59.733 [org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1] INFO o.a.k.c.producer.KafkaProducer - [Producer clientId=raw-item-producer-client-2, transactionalId=tx-5b01ad71-f754-44e0-9c52-4774a482bc1d0] Aborting incomplete transaction
2022-03-23 21:16:59.737 [org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1] INFO o.a.k.c.producer.KafkaProducer - [Producer clientId=raw-item-producer-client-1, transactionalId=tx-5b01ad71-f754-44e0-9c52-4774a482bc1draw-item-processor-group-id.OSMI_C02_CATALOG_MKPDOMAIN.0] **Aborting incomplete transaction**
2022-03-23 21:16:59.738 [org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1] ERROR o.s.k.l.KafkaMessageListenerContainer - Transaction rolled back
org.springframework.kafka.listener.ListenerExecutionFailedException: Listener method 'public void com.albertsons.mkp.rawitemprocessor.consumer.KafkaConsumer.receive(org.apache.kafka.clients.consumer.ConsumerRecord<java.lang.String, java.lang.String>) throws java.io.IOException' threw exception; nested exception is org.springframework.kafka.KafkaException: Send failed; nested exception is org.apache.kafka.common.errors.RecordTooLargeException: The message is 117440606 bytes when serialized which is larger than 1048576, which is the value of the max.request.size configuration.; nested exception is org.springframework.kafka.KafkaException: Send failed; nested exception is org.apache.kafka.common.errors.RecordTooLargeException: The message is 117440606 bytes when serialized which is larger than 1048576, which is the value of the max.request.size configuration.
2022-03-23 21:17:00.250 [org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1] INFO c.a.m.r.producer.KafkaProducer - ### --- Sending Data to Item , price, Inventory -----
2022-03-23 21:17:00.294 [org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1] ERROR o.s.k.s.LoggingProducerListener - Exception thrown when sending a message with key='String' and payload='{"sku":"String","lowestOriginalPrice":"String","lowestPrice":"String","updatedAt":"String","createdA...' to topic PRICE-TOPIC:
org.apache.kafka.common.errors.RecordTooLargeException: The message is 117440606 bytes when serialized which is larger than 1048576, which is the value of the max.request.size configuration.
2022-03-23 21:17:00.295 [org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1] INFO o.a.k.c.producer.KafkaProducer - [Producer clientId=raw-item-producer-client-2, transactionalId=tx-5b01ad71-f754-44e0-9c52-4774a482bc1d0] Aborting incomplete transaction
2022-03-23 21:17:00.298 [org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1] INFO o.a.k.c.producer.KafkaProducer - [Producer clientId=raw-item-producer-client-1, transactionalId=tx-5b01ad71-f754-44e0-9c52-4774a482bc1draw-item-processor-group-id.OSMI_C02_CATALOG_MKPDOMAIN.0] **Aborting incomplete transaction**
2022-03-23 21:17:00.308 [org.springframework.kafka.KafkaListenerEndpointContainer#0-0-C-1] ERROR o.s.k.l.**KafkaMessageListenerContainer - Transaction rolled back**
org.springframework.kafka.listener.ListenerExecutionFailedException: Listener method 'public void com.albertsons.mkp.rawitemprocessor.consumer.KafkaConsumer.receive(org.apache.kafka.clients.consumer.ConsumerRecord<java.lang.String, java.lang.String>) throws java.io.IOException' threw exception; nested exception is **org.springframework.kafka.KafkaException: Send failed**; nested exception is org.apache.kafka.common.errors.**RecordTooLargeException**: The message is 117440606 bytes when serialized which is larger than 1048576, which is the value of the max.request.size configuration.; nested exception is org.springframework.kafka.KafkaException: Send failed; nested exception is org.apache.kafka.common.errors.RecordTooLargeException: The message is 117440606 bytes when serialized which is larger than 1048576, which is the value of the max.request.size configuration.

Rolled back records remain in the log.
Kafka adds a marker to the log to indicate whether the transaction was committed or rolled back.
By default, consumers will receive all records, even if they are rolled back.
Consumers must be configured with isolation.level=read_committed to avoid seeing rolled back records.
https://kafka.apache.org/documentation/#consumerconfigs_isolation.level
Controls how to read messages written transactionally. If set to read_committed, consumer.poll() will only return transactional messages which have been committed. If set to read_uncommitted (the default), consumer.poll() will return all messages, even transactional messages which have been aborted. Non-transactional messages will be returned unconditionally in either mode.
Messages will always be returned in offset order. Hence, in read_committed mode, consumer.poll() will only return messages up to the last stable offset (LSO), which is the one less than the offset of the first open transaction. In particular any messages appearing after messages belonging to ongoing transactions will be withheld until the relevant transaction has been completed. As a result, read_committed consumers will not be able to read up to the high watermark when there are in flight transactions.
When using Spring Boot, it's read-committed, not read_committed.
spring.kafka.consumer.isolation-level=read-committed
Your IDE should suggest proper values.
Or
spring.kafka.consumer.properties=isolation.level=read_committed
EDIT
(Although I see that Boot works with read_uncommitted too).
This works as expected for me.
#SpringBootApplication
public class So71591355Application {
public static void main(String[] args) {
SpringApplication.run(So71591355Application.class, args);
}
#KafkaListener(id = "so71591355", topics = "so71591355")
void listen1(String in) {
System.out.println("committed: " + in);
}
#KafkaListener(id = "so71591355-2", topics = "so71591355",
properties = "isolation.level:read_uncommitted")
void listen2(String in) {
System.out.println("uncommitted: " + in);
}
#Bean
public NewTopic topic() {
return TopicBuilder.name("so71591355").partitions(1).replicas(1).build();
}
#Bean
ApplicationRunner runner(KafkaTemplate<String, String> template) {
template.setAllowNonTransactional(true);
return args -> {
template.send("so71591355", "non-transactional");
try {
template.executeInTransaction(t -> {
t.send("so71591355", "first");
t.send("so71591355", "second");
t.send("so71591355", new String(new byte[2000000]));
return null;
});
}
catch (Exception e) {
e.printStackTrace();
}
};
}
}
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.isolation-level=read-committed
spring.kafka.producer.transaction-id-prefix=tx-
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.6.4)
org.springframework.kafka.KafkaException: Send failed; nested exception is org.apache.kafka.common.errors.RecordTooLargeException: The message is 2000088 bytes when serialized which is larger than 1048576, which is the value of the max.request.size configuration.
at org.springframework.kafka.core.KafkaTemplate.doSend(KafkaTemplate.java:660)
at org.springframework.kafka.core.KafkaTemplate.send(KafkaTemplate.java:403)
at com.example.demo.So71591355Application.lambda$1(So71591355Application.java:49)
at org.springframework.kafka.core.KafkaTemplate.executeInTransaction(KafkaTemplate.java:507)
at com.example.demo.So71591355Application.lambda$0(So71591355Application.java:44)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:758)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:310)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301)
at com.example.demo.So71591355Application.main(So71591355Application.java:19)
Caused by: org.apache.kafka.common.errors.RecordTooLargeException: The message is 2000088 bytes when serialized which is larger than 1048576, which is the value of the max.request.size configuration.
uncommitted: non-transactional
committed: non-transactional
uncommitted: first
uncommitted: second
EDIT2
Your application is working as expected; when I add
#KafkaListener(id = "otherApp", topics = { "ITEM-TOPIC", "INVENTORY-TOPIC", "PRICE-TOPIC" })
void listen3(String in, #Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
System.out.println("so71591355 from " + topic + ": " + in);
}
to another application, it receives no data.
2022-03-24 10:04:57.939 INFO 15038 --- [ hisApp-0-C-1] o.s.k.l.KafkaMessageListenerContainer : otherApp: partitions assigned: [PRICE-TOPIC-0, ITEM-TOPIC-0, INVENTORY-TOPIC-0]
Of course, with a console consumer, we see the messages because the console consumer is not read_committed.
And when I comment out the price send; I see
so71591355 from INVENTORY-TOPIC: Inventory data : My test Message
so71591355 from ITEM-TOPIC: Item data : My test Message
...
EDIT3
To customize the after rollback processor; simply add it as a #Bean and Boot will wire it into the container factory.
#Bean
AfterRollbackProcessor<Object, Object> arp() {
return new DefaultAfterRollbackProcessor<>((rec, ex) -> {
log.error("Failed to process {} from topic, partition {}-{}, #{}",
rec.value(), rec.topic(), rec.partition(), rec.offset(), ex);
}, new FixedBackOff(3000L, 2));
}
However, you should remove the excuteInTransaction call and just do the sends directly on the template. That way, the template will participate in the container's transaction instead of starting a new one.
This example just logs the error; you can add DeadLetterPublishingRecoverer (or any custom recoverer).

Related

Error Handling in Transactional Kafka Listeners

This question follows my post on Request/Reply and Retry Policy for Kafka Listeners but in the context of Transactional Kafka Listeners (the current implementation is therefore similar to the proposed solution).
Basically, the idea is to be able to support a complete error management which is, based on the type of exception, either retry X times the record or send it to a dead letter topic for exception raised inside a Kafka Listener tagged with #Transactional.
When I specify the errorHandler parameter to my #KafkaListener, I can see that it goes through my logic for the first time but then, after sending to the dead letter topic (and returning my custom response in case of #SendTo), it rolls back the transaction and retries to process my record as defined by the BackOff period of the DefaultAfterRollbackProcessor.
Is there anyway to prevent these retries in the case the exception has been properly handled and then just carry on with the next transaction ?
Here are my various handlers defined as suggested by the solution in the above link:
#Bean
public ErrorHandler errorHandler(MyDeadLetterQueueHandler deadLetterQueueHandler) {
//set with retry policy higher than KafkaListenerErrorHandler
return new SeekToCurrentErrorHandler((data, thrownException) -> {
deadLetterQueueHandler.send(data, thrownException);
}, new FixedBackOff(15000, 20));
}
#Bean
public AfterRollbackProcessor<?, ?> afterRollbackProcessor(MyDeadLetterQueueHandler deadLetterQueueHandler) {
//set with retry policy higher than KafkaListenerErrorHandler
final var afterRollbackProcessor = new DefaultAfterRollbackProcessor<Object, Object>(((data, thrownException) -> {
deadLetterQueueHandler.send(data, thrownException);
}, new FixedBackOff(15000, 20));
afterRollbackProcessor.setCommitRecovered(true);
return afterRollbackProcessor;
}
#Primary
KafkaListenerErrorHandler kafkaListenerErrorHandler(MyDeadLetterQueueHandler deadLetterQueueHandler,
MyExceptionHandler exceptionHandler) {
return (message, exception) -> {
final var cause = (Exception) exception.getCause();
final var consumerRecord = message.getHeaders().get(KafkaHeaders.RAW_DATA, ConsumerRecord.class);
if (shouldGoToDLT(cause)) {
sendToDeadLetterTopic(deadLetterQueueHandler, consumerRecord, cause);
return new CustomResponse(cause.getMessage());
// should end transaction rollback and go to next transaction
} else {
// retry 10 times before killing the app
var deliveryAttempt = message.getHeaders().get(KafkaHeaders.DELIVERY_ATTEMPT, Integer.class);
if (deliveryAttempt > 10) {
exceptionHandler.handle(cause);
}
}
throw exception;
};
}
and the logs that I get from my test using EmbeddedKafkaBroker and throwing an exception in a #Transactional Kafka Listener:
2021-07-01 17:12:34.791 INFO [,0beec62e5e3dbb97,0beec62e5e3dbb97] 19210 --- [ntainer#1-0-C-1] o.a.k.clients.producer.KafkaProducer : [Producer clientId=producer-consumer-group.rollback-db-employee-topic.0, transactionalId=consumer-group.rollback-db-employee-topic.0] Aborting incomplete transaction
2021-07-01 17:12:34.812 ERROR [,0beec62e5e3dbb97,0beec62e5e3dbb97] 19210 --- [ntainer#1-0-C-1] essageListenerContainer$ListenerConsumer : Transaction rolled back
org.springframework.transaction.HeuristicCompletionException: Heuristic completion: outcome state is rolled back; nested exception is org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
at org.springframework.data.transaction.ChainedTransactionManager.commit(ChainedTransactionManager.java:195)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:152)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeInTransaction(KafkaMessageListenerContainer.java:2072)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeRecordListenerInTx(KafkaMessageListenerContainer.java:2041)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeRecordListener(KafkaMessageListenerContainer.java:2017)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeListener(KafkaMessageListenerContainer.java:1702)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeIfHaveRecords(KafkaMessageListenerContainer.java:1272)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.pollAndInvoke(KafkaMessageListenerContainer.java:1264)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:1161)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:752)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
at org.springframework.data.transaction.MultiTransactionStatus.commit(MultiTransactionStatus.java:74)
at org.springframework.data.transaction.ChainedTransactionManager.commit(ChainedTransactionManager.java:168)
... 11 common frames omitted
2021-07-01 17:12:34.918 INFO [,0beec62e5e3dbb97,0beec62e5e3dbb97] 19210 --- [ntainer#1-0-C-1] o.a.k.clients.consumer.KafkaConsumer : [Consumer clientId=consumer-consumer-group-3, groupId=consumer-group] Seeking to offset 0 for partition rollback-db-employee-topic-0
2021-07-01 17:12:34.929 INFO [,0beec62e5e3dbb97,2d5e98ce0b91d04a] 19210 --- [ntainer#1-0-C-1] o.a.k.clients.producer.KafkaProducer : [Producer clientId=producer-consumer-group.rollback-db-employee-topic.0, transactionalId=consumer-group.rollback-db-employee-topic.0] Aborting incomplete transaction
2021-07-01 17:12:34.931 ERROR [,0beec62e5e3dbb97,2d5e98ce0b91d04a] 19210 --- [ntainer#1-0-C-1] essageListenerContainer$ListenerConsumer : Transaction rolled back
org.springframework.transaction.HeuristicCompletionException: Heuristic completion: outcome state is rolled back; nested exception is org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
at org.springframework.data.transaction.ChainedTransactionManager.commit(ChainedTransactionManager.java:195)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:152)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeInTransaction(KafkaMessageListenerContainer.java:2072)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeRecordListenerInTx(KafkaMessageListenerContainer.java:2041)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeRecordListener(KafkaMessageListenerContainer.java:2017)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeListener(KafkaMessageListenerContainer.java:1702)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeIfHaveRecords(KafkaMessageListenerContainer.java:1272)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.pollAndInvoke(KafkaMessageListenerContainer.java:1264)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:1161)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:752)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
at org.springframework.data.transaction.MultiTransactionStatus.commit(MultiTransactionStatus.java:74)
at org.springframework.data.transaction.ChainedTransactionManager.commit(ChainedTransactionManager.java:168)
... 11 common frames omitted
2021-07-01 17:12:35.034 INFO [,0beec62e5e3dbb97,2d5e98ce0b91d04a] 19210 --- [ntainer#1-0-C-1] o.a.k.clients.consumer.KafkaConsumer : [Consumer clientId=consumer-consumer-group-3, groupId=consumer-group] Seeking to offset 0 for partition rollback-db-employee-topic-0
2021-07-01 17:12:35.445 INFO [,0beec62e5e3dbb97,4ef5c58e90699a09] 19210 --- [ntainer#1-0-C-1] o.a.k.clients.producer.KafkaProducer : [Producer clientId=producer-consumer-group.rollback-db-employee-topic.0, transactionalId=consumer-group.rollback-db-employee-topic.0] Aborting incomplete transaction
2021-07-01 17:12:35.448 ERROR [,0beec62e5e3dbb97,4ef5c58e90699a09] 19210 --- [ntainer#1-0-C-1] essageListenerContainer$ListenerConsumer : Transaction rolled back
org.springframework.transaction.HeuristicCompletionException: Heuristic completion: outcome state is rolled back; nested exception is org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
at org.springframework.data.transaction.ChainedTransactionManager.commit(ChainedTransactionManager.java:195)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:152)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeInTransaction(KafkaMessageListenerContainer.java:2072)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeRecordListenerInTx(KafkaMessageListenerContainer.java:2041)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeRecordListener(KafkaMessageListenerContainer.java:2017)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeListener(KafkaMessageListenerContainer.java:1702)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeIfHaveRecords(KafkaMessageListenerContainer.java:1272)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.pollAndInvoke(KafkaMessageListenerContainer.java:1264)
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:1161)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:752)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
at org.springframework.data.transaction.MultiTransactionStatus.commit(MultiTransactionStatus.java:74)
at org.springframework.data.transaction.ChainedTransactionManager.commit(ChainedTransactionManager.java:168)
... 11 common frames omitted
…
Thanks for your help.
EDIT: Here's my listener:
#KafkaListener(topics = "user-topic", groupId = "consumer-group-1", errorHandler ="errorHandler")
#Transactional
public void onReceive(User command) {
// update database
userRepository.save(command);
switch (command.getName()) {
case "GOTODLT":
var volunteerArithmeticException = 7 / 0;
break;
case "SHOULDRETRY":
throw new IllegalStateException("Should be retried 10 times");
}
}
It is not clear why you are using #Transactional since you are using a ChainedKafkaTransactionManager (which is deprecated by the way; see the parent class javadocs). It is ok to use it, as long as you are aware of the limitations.
The transactions have already been started by the transaction managers, so the annotation is not needed.
Since your listener method is wrapped in a transaction interceptor, the transaction is rolled back before your listener error handler is invoked.
Hence the Transaction silently rolled back because it has been marked as rollback-only message.
Remove the annotation and it should work as you expect.
You should not configure both a STCEH and ARP. The former runs inside the transaction, the latter after a rollback.

Spring Boot + Atomikos: Exception thrown in a transactional JMS listener inside try-catch immediately causes rollback

I'm using Spring Boot 1.5.3 and Atomikos 4.0.6. I have a DefaultMessageListenerContainer setup to listen a JMS queue and handle incoming messages in a transaction using the Spring JTATransactionManager configured to use Atomikos. The message handler calls a transactional service that tries to process the message inside a try-catch-block and in case of exceptions calls other transactional methods for logging purposes. The idea is that the transaction should be rolled back only after everything is logged and the encountered RuntimeException is thrown from the catch-block.
#Transactional
public void handleMessage(UnmarshalledMessage message) {
try {
Thing thing = repository.find(message.getId());
...
}
catch (Exception e) {
// NoResultException translated into EmptyResultDataAccessException
logger.logUsingTransactions(e.getMessage());
throw e;
}
}
However, what happens that the transaction is rolled back immediately after it's originally thrown inside repository.find(). When attempting to read from the database inside the catch-block, an exception is thrown since the transaction has been marked as rollback only:
c.atomikos.jdbc.AtomikosSQLException - Transaction is marked for rollback only or has timed out
com.atomikos.datasource.xa.session.InvalidSessionHandleStateException: Transaction is marked for rollback only or has timed out
at com.atomikos.datasource.xa.session.NotInBranchStateHandler.checkEnlistBeforeUse(NotInBranchStateHandler.java:39)
at com.atomikos.datasource.xa.session.TransactionContext.checkEnlistBeforeUse(TransactionContext.java:70)
at com.atomikos.datasource.xa.session.SessionHandleState.notifyBeforeUse(SessionHandleState.java:160)
at com.atomikos.jdbc.AtomikosConnectionProxy.enlist(AtomikosConnectionProxy.java:207)
at com.atomikos.jdbc.AtomikosConnectionProxy.invoke(AtomikosConnectionProxy.java:122)
at com.sun.proxy.$Proxy129.prepareStatement(Unknown Source)
at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.prepareStatement(DatabaseAccessor.java:1565)
at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.prepareStatement(DatabaseAccessor.java:1514)
at org.eclipse.persistence.internal.databaseaccess.DatabaseCall.prepareStatement(DatabaseCall.java:778)
at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.basicExecuteCall(DatabaseAccessor.java:621)
at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.executeCall(DatabaseAccessor.java:560)
at org.eclipse.persistence.internal.sessions.AbstractSession.basicExecuteCall(AbstractSession.java:2055)
at org.eclipse.persistence.sessions.server.ServerSession.executeCall(ServerSession.java:570)
at org.eclipse.persistence.sessions.server.ClientSession.executeCall(ClientSession.java:258)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:242)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:228)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.selectOneRow(DatasourceCallQueryMechanism.java:714)
at org.eclipse.persistence.internal.queries.ExpressionQueryMechanism.selectOneRowFromTable(ExpressionQueryMechanism.java:2803)
at org.eclipse.persistence.internal.queries.ExpressionQueryMechanism.selectOneRow(ExpressionQueryMechanism.java:2756)
at org.eclipse.persistence.queries.ReadObjectQuery.executeObjectLevelReadQuery(ReadObjectQuery.java:555)
at org.eclipse.persistence.queries.ObjectLevelReadQuery.executeDatabaseQuery(ObjectLevelReadQuery.java:1175)
at org.eclipse.persistence.queries.DatabaseQuery.execute(DatabaseQuery.java:904)
at org.eclipse.persistence.queries.ObjectLevelReadQuery.execute(ObjectLevelReadQuery.java:1134)
at org.eclipse.persistence.queries.ReadObjectQuery.execute(ReadObjectQuery.java:441)
at org.eclipse.persistence.queries.ObjectLevelReadQuery.executeInUnitOfWork(ObjectLevelReadQuery.java:1222)
at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.internalExecuteQuery(UnitOfWorkImpl.java:2896)
at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1857)
at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1839)
at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1790)
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.executeQuery(EntityManagerImpl.java:911)
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.findInternal(EntityManagerImpl.java:854)
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.find(EntityManagerImpl.java:730)
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.find(EntityManagerImpl.java:599)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
I'd like to know what is causing this behavior and how to resolve the issue. Note that this setup has been working correctly when run in Weblogic. For additional information, here is a transaction trace log when the exception is first encountered inside the repository method.
DEBUG o.s.t.jta.JtaTransactionManager - Initiating transaction commit
DEBUG o.s.t.jta.JtaTransactionManager - Creating new transaction with name [myMessageListenerContainer]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG o.s.t.jta.JtaTransactionManager - Participating in existing transaction
TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [my.transactional.messagehandler.handleMessage]
DEBUG o.s.t.jta.JtaTransactionManager - Participating in existing transaction
TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [my.repository.class.method]
TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [my.repository.class.method] after exception: org.springframework.dao.EmptyResultDataAccessException: ProcessableMessage with id 443e73e7-0905-416b-9e03-4aaa2bbf09fb; nested exception is javax.persistence.NoResultException: ProcessableMessage with id [message-id]
TRACE o.s.t.i.RuleBasedTransactionAttribute - Applying rules to determine whether transaction should rollback on org.springframework.dao.EmptyResultDataAccessException: ProcessableMessage with id 443e73e7-0905-416b-9e03-4aaa2bbf09fb; nested exception is javax.persistence.NoResultException: ProcessableMessage with id [message-id]
TRACE o.s.t.i.RuleBasedTransactionAttribute - Winning rollback rule is: null
TRACE o.s.t.i.RuleBasedTransactionAttribute - No relevant rollback rule found: applying default rules
DEBUG o.s.t.jta.JtaTransactionManager - Participating transaction failed - marking existing transaction as rollback-only
DEBUG o.s.t.jta.JtaTransactionManager - Setting JTA transaction rollback-only
EDIT:
I am using JPA and the NoResultException is initially thrown in a following way:
public static <T> T mandatoryFind(EntityManager em, Class<T> type, Object id) throws NoResultException {
T found = em.find(type, id);
if (found == null) {
throw new NoResultException(type.getSimpleName() +" with id "+ id);
}
return found;
}
Which in turn is called from a class annotated with #Transactional(noRollbackFor = NoResultException.class). The exception is raised in a fairly normal use case - what I'm troubled with is why the transaction is rolled back before I can handle the exception?
It sounds like you do use JPA under the hood (based on your debug logs).
This is a typical behaviour then you do query a single result.
NoResultException:
Thrown by the persistence provider when Query.getSingleResult() or TypedQuery.getSingleResult()is executed on a query and there is no result to return. This exception will not cause the current transaction, if one is active, to be marked for rollback.
In order to avoid that behaviour you do query result as a List.

Spring Integration JMS with JTA rollback although exception is handled in ErrorHandler

I am using Spring Integration with JTA support through Atomikos and JMS bound to different Webshpere MQ and a Oracle Database.
Obviosly for others it seems to be the same thread as in Spring Integration JMS with JTA rollback when message goes to errorChannel but it isn't not at all.
The flow is the following:
message-driven-channel-adapter receive the message within transaction
some transformations
ServiceActivator with deeper business logic
DataBase Update
Everything works really fine expect for one szenario:
If an unchecked exception occurs (maybe due to inkonsistent data which shouldn't be) within the ServiceActivator the message is rethrown and handled within an ErrorHandler (via ErrorChannel).
There - in some cases - the orgininal incoming message should be send to a DeadLetter Queue (Webshere MQ). This is done with outbound-channel-adapter.
See enclosed configuration:
<jms:message-driven-channel-adapter id="midxMessageDrivenAdapter"
channel="midxReplyChannel"
acknowledge="auto"
transaction-manager="transactionManager"
connection-factory="connectionFactory"
concurrent-consumers="1"
error-channel="errorChannel"
max-concurrent-consumers="${cmab.integration.midx.max.consumer}"
idle-task-execution-limit="${cmab.integration.midx.idleTaskExecutionLimit}"
receive-timeout="${cmab.integration.midx.receiveTimeout}"
destination="midxReplyQueue"/>
................
<int:service-activator input-channel="midxReplyProcessChannel" ref="processMidxReplyDbWriter" />
<int:service-activator input-channel="errorChannel" ref="inputErrorHandler" />
<jms:outbound-channel-adapter id="deadLetterOutboundChannelAdapter"
channel="errorDlChannel" destination="deadLetterQueue"
delivery-persistent="true" connection-factory="nonXAConnectionFactory"/>
Some important hints:
message-driven-channel-adapter:
connectionFactory is a MQXAQueueConnectionFactory wihtin an AtomikosConnectionFactoryBean
transaction-manager is Spring JtaTransactionManager
outbound-channel-adapter:
connection-factory is a nonXAConnectionFactory.
CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory();
MQQueueConnectionFactory mqQueueConnectionFactory = new MQQueueConnectionFactory();
.....
cachingConnectionFactory.setTargetConnectionFactory(mqQueueConnectionFactory);
return cachingConnectionFactory;
And now the error which I observed:
Although I handled the unchecked exception in my ErrorHandler (ref="inputErrorHandler"; send the message to DeadLetter-Queue) an rollback is initiated and the message-driven-channel-adapter receives the message again and again.
Curious is the fact that indeed the message is delivered to the DeadLetterQueue via the outbound-channel-adapter. The destination (deadLetterQueue) contains the failed messages.
Question: What I'm doing wrong? The failed original incoming message is rolled back although I handled the exception in my ErrorHandler.
Any help really appreciate. Many thanks in advance.
concerning to my comment see enclose code of my InputErrorHandler:
if (throwable instanceof MessageHandlingException) {
MessageHandlingException messageHandlingException = (MessageHandlingException) throwable;
if (messageHandlingException.getCause() != null
&& (messageHandlingException.getCause() instanceof CmabProcessDbException
|| messageHandlingException.getCause() instanceof CmabReplyFormatException)) {
String appMessage = ((CmabException) messageHandlingException.getCause()).getAppMessagesAsString();
LOGGER.error(String.format("cmab rollback exception occured: %s", appMessage));
LOGGER.error("******** initiate rollback *********");
throw throwable.getCause();
} else {
ErrorDto payload = fillMessagePayload(messageHandlingException, throwableClassName);
sendMessageToTargetQueue(payload);
}
As I mentioned the "business" ServiceActivator throws an unchecked exception so in this case the ELSE-Statements are calling.
Within that I build up a Message with MessagBuilder and sends it ti the errorDlChannel (s. above the outbound-channel-adapter!).
private void sendMessageToDeadLetterQueue(Message<?> message, String description, String cause) {
.......
Message<?> mb =
MessageBuilder.withPayload(message.getPayload())
.copyHeaders(message.getHeaders())
.setHeaderIfAbsent("senderObject", this.getClass().getName())
.setHeaderIfAbsent("errorText", description)
.setHeaderIfAbsent("errorCause", errorCause).build();
errorDlChannel.send(mb);
}
That's all. This is for this case the last statement. There is nothing anything else in the main method of my ErrorHandler. No rethrow or other stuff.
So this is the reason of my confusion. For me the exception is handled by sending it to the errorDlChannel (outbound-channel-adapter -> DeadLetterQueue).
I saw the message on the DeadLetter Queue but nevertheless a jta rollback occurs...and IMO this shouldn't bee.

Spring JMS transaction rollback - message dequeued off ActiveMQ

I have a simple Spring Boot application(Spring Boot Version 1.5.3.RELEASE) for consuming JMS Messages off an ActiveMQ(version 5.14.5) Queue.
I want the messages to be consumed in a JMS transaction. If there is an exception during message consumption, I expect transaction to be rolled back and message not to be dequeued(taken off message queue). I can see transaction being rolled back in Spring logs, however the message is still dequeued from ActiveMQ queue(after six re delivery attempts).
Any pointers will be appreciated.
Here is the application code:
#SpringBootApplication
public class SpringJmsDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringJmsDemoApplication.class, args);
}
#Bean
public JmsListenerContainerFactory<?> myFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory defaultJmsListenerContainerFactory = new DefaultJmsListenerContainerFactory();
defaultJmsListenerContainerFactory.setTransactionManager(jmsTransactionManager(connectionFactory));
defaultJmsListenerContainerFactory.setSessionTransacted(true);
defaultJmsListenerContainerFactory.setSessionAcknowledgeMode(Session.SESSION_TRANSACTED);
configurer.configure(defaultJmsListenerContainerFactory, connectionFactory);
return defaultJmsListenerContainerFactory;
}
#Bean
public PlatformTransactionManager jmsTransactionManager(ConnectionFactory connectionFactory) {
return new JmsTransactionManager(connectionFactory);
}
}
#Component
public class Receiver {
#JmsListener(destination = "mailbox", containerFactory = "myFactory")
#Transactional
public void receiveMessage(String email) {
System.out.println("Received <" + email + ">");
throw new RuntimeException("nooo");
}
}
Here is the log:
2017-05-24 09:51:59.865 DEBUG 8972 --- [DefaultMessageListenerContainer-1] o.s.j.connection.JmsTransactionManager : Created JMS transaction on Session [ActiveMQSession {id=ID:D6C0B8467A518-58248-1495590693980-1:32:1,started=false} java.lang.Object#65d647cd] from Connection [ActiveMQConnection
2017-05-24 09:51:59.867 DEBUG 8972 --- [DefaultMessageListenerContainer-1] o.s.j.l.DefaultMessageListenerContainer : Received message of type [class org.apache.activemq.command.ActiveMQTextMessage] from consumer [ActiveMQMessageConsumer { value=ID:D6C0B8467A518-58248-1495590693980-1:32:1:1, started=true }] of transactional session [ActiveMQSession {id=ID:D6C0B8467A518-58248-1495590693980-1:32:1,started=true} java.lang.Object#65d647cd]
2017-05-24 09:51:59.867 DEBUG 8972 --- [DefaultMessageListenerContainer-1] o.s.j.l.DefaultMessageListenerContainer : Rolling back transaction because of listener exception thrown: org.springframework.jms.listener.adapter.ListenerExecutionFailedException: Listener method 'public void com.anz.markets.springjmsdemo.Receiver.receiveMessage(java.lang.String)' threw exception; nested exception is java.lang.RuntimeException: nooo
2017-05-24 09:51:59.867 WARN 8972 --- [DefaultMessageListenerContainer-1] o.s.j.l.DefaultMessageListenerContainer : Execution of JMS message listener failed, and no ErrorHandler has been set.
org.springframework.jms.listener.adapter.ListenerExecutionFailedException: Listener method 'public void com.anz.markets.springjmsdemo.Receiver.receiveMessage(java.lang.String)' threw exception; nested exception is java.lang.RuntimeException: nooo
at org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:112) ~[spring-jms-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter.onMessage(MessagingMessageListenerAdapter.java:69) ~[spring-jms-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at org.springframework.jms.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:721) ~[spring-jms-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at org.springframework.jms.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:681) ~[spring-jms-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at org.springframework.jms.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:651) ~[spring-jms-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.doReceiveAndExecute(AbstractPollingMessageListenerContainer.java:317) [spring-jms-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.receiveAndExecute(AbstractPollingMessageListenerContainer.java:235) [spring-jms-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.invokeListener(DefaultMessageListenerContainer.java:1166) [spring-jms-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.executeOngoingLoop(DefaultMessageListenerContainer.java:1158) [spring-jms-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.run(DefaultMessageListenerContainer.java:1055) [spring-jms-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_72]
Caused by: java.lang.RuntimeException: nooo
at com.anz.markets.springjmsdemo.Receiver.receiveMessage(Receiver.java:12) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_72]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_72]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_72]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_72]
at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:180) ~[spring-messaging-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:112) ~[spring-messaging-4.3.8.RELEASE.jar:4.3.8.RELEASE]
at org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:104) ~[spring-jms-4.3.8.RELEASE.jar:4.3.8.RELEASE]
... 10 common frames omitted
2017-05-24 09:51:59.868 DEBUG 8972 --- [DefaultMessageListenerContainer-1] o.s.j.connection.JmsTransactionManager : Transactional code has requested rollback
2017-05-24 09:51:59.868 DEBUG 8972 --- [DefaultMessageListenerContainer-1] o.s.j.connection.JmsTransactionManager : Initiating transaction rollback
2017-05-24 09:51:59.868 DEBUG 8972 --- [DefaultMessageListenerContainer-1] o.s.j.connection.JmsTransactionManager : Rolling back JMS transaction on Session [ActiveMQSession {id=ID:D6C0B8467A518-58248-1495590693980-1:32:1,started=true} java.lang.Object#65d647cd]
According to ActiveMQ message re-delivery documentation, the messages, which failed to be delivered, will go to the dead letter queue (http://activemq.apache.org/message-redelivery-and-dlq-handling.html):
"The default Dead Letter Queue in ActiveMQ is called ActiveMQ.DLQ; all un-deliverable messages will get sent to this queue and this can be difficult to manage. So, you can set an individualDeadLetterStrategy in the destination policy map of the activemq.xml configuration file, which allows you to specify a specific dead letter queue prefix for a given queue or topic. You can apply this strategy using wild card if you like so that all queues get their own dead-letter queue, as is shown in the example below"
Please, extend you activemq.xml with individualDeadLetterStrategy to the queue.

Tibco +Spring JMS behavior with ErrorHandler on TransactionRolledBackException

I would like to clarify behavior of the Tibco bus (6.1) used via Spring (4.1) JMS config - with and without ErrorHandler specified, and for a specific case of the TransactionRolledBackException.
I configure JMS listener with the "transacted" mode as following:
<jms:listener-container connection-factory="singleConnectionFactory"             
acknowledge="transacted" task-executor="myTaskExecutor" concurrency="${my.queue.concurrency}">
<jms:listener destination="${queue.destination}" ref="myProcessor" method="processMyMessage" />
</jms:listener-container>
and also set 'maxRedelivery' for the queue to 2.
Here are the scenarios of processing an event in the myProcessor.processMyMessage():
message is processed successfully; Result: message is ACK'ed and removed from the queue
a Runtime exception is thrown , either by business logic or by the Spring container (say OOM or HectorException); Result: message is not ACK'ed, put back on the bus and redelivered up to 2 times
org.springframework.jms.TransactionRolledBackException is thrown (by Spring, I assume) - since it is a Runtime exception the resulting behavior is the same as in 2. (is this correct?)
Now, I am adding an explicit ErrorHandler to better log some runtime exceptions:
<jms:listener-container connection-factory="singleConnectionFactory"             
acknowledge="transacted" task-executor="myTaskExecutor" concurrency="${my.queue.concurrency}"
error-handler="myErrorHandler">
<jms:listener destination="${queue.destination}" ref="myProcessor" method="processMyMessage" />
</jms:listener-container>
And the following implementation of the ErrorHandler, where we swallow TransactionRolledBackException:
import org.springframework.jms.TransactionRolledBackException;
import org.springframework.util.ErrorHandler;
public class JMSListenerErrorHandler implements ErrorHandler {
#Override
public void handleError(Throwable t) {
if (t.getCause() instanceof TransactionRolledBackException) {
log.warn("JMS transaction rollback; reason: " + t.getMessage(), t);
} else {
log.error(t.getMessage(), t);
throw new RuntimeException(t);
}
}
What is going to happen for each of the use cases? Here is my guess:
no change
no change - this Runtime exception is re-thrown, so the message will not be ACK'ed and will be redelivered up to 2 time
TransactionRolledBackException: when I handle this exception in the ErrorHandler - is this happening AFTER the TX was already rolled back (as the name RolledBack suggests) and message was marked as not ACK'ed, thus, causing it to be redelivered as in the case 2. ? Or do I effectively cause this messaged to be ACK'ed by swallowing this exception?
Couple more questions:
under what conditions would Spring throw an org.springframework.jms.TransactionRolledBackException ?
is TransactionRolledBackException treated any different than any other Runtime exception?
Thank you,
Marina
Spring JMS only throws that exception when the underlying JMS client throws a javax.jms.TransactionRolledBackException - it is simply an unchecked wrapper exception - see JmxUtils.convertJmsAccessException().
Take a look at the stack trace and you will see where/why it was thrown.
In general, i.e. if it was thrown on the container's session, catching it won't have any effect - the transaction is already rolled back; but the stack trace is the key.

Resources