2

I have a Spring Boot application which consumes messages from queue (ActiveMQ) and writes them to the database (DB2) and I need it to be fully transactional. I got to a point where I understood that transaction manager (using spring-boot-starter-jta-atomikos) is a best solution for distributed transactions and I'm trying to correctly implement it.

JMS configuration class:

@EnableJms
@Configuration
public class MQConfig {

  @Bean
  public ConnectionFactory connectionFactory() {
    RedeliveryPolicy rp = new RedeliveryPolicy();
    rp.setMaximumRedeliveries(3);
    rp.setRedeliveryDelay(1000L);

    ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory();
    cf.setBrokerURL("tcp://localhost:61616");
    cf.setRedeliveryPolicy(rp);
    return cf;
  }

  @Bean
  public JmsTemplate jmsTemplate() {
    JmsTemplate template = new JmsTemplate(connectionFactory());
    template.setConnectionFactory(connectionFactory());
    return template;
  }

  @Bean
  public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
    DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory());
    factory.setCacheLevelName("CACHE_CONSUMER");
    factory.setReceiveTimeout(1000L);
    factory.setSessionTransacted(true);
    return factory;
  }
}

JMS listener class:

@Component
public class MQListener {

  @Autowired
  private ImportRecordsService importRecordsService;

  @JmsListener(
        containerFactory = "jmsListenerContainerFactory",
        destination = "test.queue"
        // concurrency = "4-10"
  )
  public void receiveMessage(TextMessage message) throws JMSException {
    importRecordsService.createRecord();
  }
}

Service class that writes to DB:

@Service
public class ImportRecordsService {

  @Autowired
  private ImportRecordsDAO dao;

  @Transactional
  public void createRecord() {
    ImportRecord record = new ImportRecord();
    record.setDateCreated(LocalDateTime.now());
    record.setName("test-001");
    dao.save(record);
  }
}

If exception is thrown inside createRecord() after save, rollback works as it should. When an exception is thrown inside JMS listener in receiveMessage() after save, message is returned to queue but database record stays.

Any help greatly appreciated.

juststeve
  • 21
  • 4

1 Answers1

1

This should be as simple as adding your transactionManager to your DefaultJmsListenerContainerFactory.

In this case add the PlatformTransactionManager (should be an available Spring Bean) to your jmsListenerContainerFactory method signature and then call factory.setTransactionManager(jtaTransactionManager).

  @Bean
  public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory, PlatformTransactionManager jtaTransactionManager) {
    DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setCacheLevelName("CACHE_CONSUMER");
    factory.setReceiveTimeout(1000L);
    factory.setTransactionManager(jtaTransactionManager);
    factory.setSessionTransacted(true);
    return factory;
  }

Although you will need to note that setting the transactionManager will reset your cache level to CACHE_NONE.