0

we migrated a websphere j2ee app to spring boot. Everything looked great but now we found out that the message listeners are sometimes processing some messages twice.

It looks like to me it happens when one message is been processed and not yet commited, an other concurrent consumer can get the same message and also process it. Looks like the message broker doesn't hold it back, doesn't reserves it for consumer 1.

import bitronix.tm.resource.jms.PoolingConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.listener.DefaultMessageListenerContainer;
import org.springframework.transaction.PlatformTransactionManager;

import javax.jms.ConnectionFactory;
import javax.jms.Session;
import java.util.Properties;

@Configuration
@EnableJms
@EnableCaching(proxyTargetClass = true)
public class JmsConfig {

    @Bean
    ConnectionFactory jmsXAConnectionFactory() {
        PoolingConnectionFactory connectionFactory = new PoolingConnectionFactory();
        connectionFactory.setClassName("com.ibm.mq.jms.MQXAQueueConnectionFactory");
        connectionFactory.setUniqueName("mq-xa-" + appName);
        connectionFactory.setAllowLocalTransactions(true);
        connectionFactory.setTestConnections(false);
        connectionFactory.setUser(user);
        connectionFactory.setPassword(password);
        connectionFactory.setMaxIdleTime(1800);
        connectionFactory.setMinPoolSize(1);
        connectionFactory.setMaxPoolSize(25);
        connectionFactory.setAcquisitionTimeout(60);
        connectionFactory.setAutomaticEnlistingEnabled(true);
        connectionFactory.setDeferConnectionRelease(true);
        connectionFactory.setShareTransactionConnections(false);
        
        Properties driverProperties = connectionFactory.getDriverProperties();
        driverProperties.setProperty("queueManager", queueManager);
        driverProperties.setProperty("hostName", connName);
        driverProperties.setProperty("port", "1414");
        driverProperties.setProperty("channel", channel);
        driverProperties.setProperty("transportType", "1"); 
        driverProperties.setProperty("messageRetention", "1"); 
        
        return connectionFactory;
    }
    
    @Primary 
    @Bean
    public BitronixTransactionManager btronixTransactionManager() throws SystemException {
        TransactionManagerServices.getConfiguration().setServerId("bitronix-tm-" + appName);
        TransactionManagerServices.getConfiguration().setLogPart1Filename(jtaLogDir + "/btm1.tlog");
        TransactionManagerServices.getConfiguration().setLogPart2Filename(jtaLogDir + "/btm2.tlog");
        TransactionManagerServices.getTransactionManager().setTransactionTimeout(180);
        return TransactionManagerServices.getTransactionManager();
    }

    @Bean
    public PlatformTransactionManager platformTransactionManager(
            BitronixTransactionManager transactionManager, UserTransaction userTransaction) {
        return new JtaTransactionManager(userTransaction, transactionManager);
    }

    @Bean("wgstatusML")
    public DefaultMessageListenerContainer wagenstatusMessageListenerContainer(
            ConnectionFactory jmsXAConnectionFactory,
            PlatformTransactionManager jtaTransactionManager,
            @Qualifier("wagenstatusBean") WagenstatusBean wagenstatusBean) {
        DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
        container.setConnectionFactory(jmsXAConnectionFactory);
        container.setTransactionManager(jtaTransactionManager);
        container.setDestinationName(WAGENSTATUS_QUEUE);
        container.setMessageListener(wagenstatusBean);
        container.setAutoStartup(false);
        container.setConcurrentConsumers(2);
        container.setClientId("wgstatListener");
        container.setSessionTransacted(false);
        container.setSessionAcknowledgeMode(Session.AUTO_ACKNOWLEDGE);
        return container;
    }
}

@Service("wagenstatusBean")
@Scope(SCOPE_PROTOTYPE)
public class WagenstatusBean extends AbstractMDB {

    @Transactional(propagation = REQUIRED)
    public void onMessage(javax.jms.Message msg) {
        String localMessageText = null;
        try {
            try {
                localMessageText = ((TextMessage) msg).getText();
            } catch (JMSException e) {
            }

            // here goes the actual call to the impl
            String errmsg = null;
            readableMessageID = null;
            try {
                verarbeiteMeldung(msg);
            } catch (InvalidMessageException ime) {
                errmsg = ime.getMessage();
            }

            if (sendMessageToErrorQueue) {
                // generate business logging entry
                try {
                    logBusinessData(localMessageText, BusinessLogger.STATUS_ERROR);
                } catch (Exception e) {
                    LOGGER.error("", e);
                }

                if (localMessageText != null) {
                    localMessageText = this.addErrorMessageToXML(localMessageText, errmsg);
                }

                DispatcherServiceLocator.getDispatcherBean().sendToDestination(
                        QueueNames.WAGENSTATUS_ERROR_QUEUE, localMessageText);
            }
        } catch (ConsistencyException ex) {
            // ConsistencyException used internally in the EJBEnv
            // framework/template needs to be catched and translated into an EJBException
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();;

            // generate business logging entry
            try {
                // UE03772, RfC 169: BUSINESS LOGGING POINT
                logBusinessData(localMessageText, BusinessLogger.STATUS_ERROR);
            } catch (Exception e) {
                LOGGER.error("", e);
            }
            LOGGER.error("Caught a ConsistencyException in WagenStatus-onMessage", ex);
        } catch (RuntimeException ex) {
            // this catching is done for logging purpouse only.
            LOGGER.error("Caught a RuntimeException in WagenStatus-onMessage", ex);

            // generate business logging entry
            try {
                logBusinessData(localMessageText, BusinessLogger.STATUS_ERROR);
            } catch (Exception e) {
                LOGGER.error("", e);
            }
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();;
        }

    } 
myborobudur
  • 4,385
  • 8
  • 40
  • 61
  • I am not familiar with Bitronix JTA transaction manager. You did not mention how you configured Bitronix with IBM MQ. This must be done, otherwise transaction handling does not work. I generally try to avoid XA when consuming MQ messages. I always followed recommendation of Java API of [AbstractMessageListenerContainer](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jms/listener/AbstractMessageListenerContainer.html) and use `sessionTransacted = true`. – Daniel Steinmann Aug 31 '22 at 13:16
  • You also should not use a caching connection factory with dynamic scaling as recommended in the Java API of [DefaultMessageListenerContainer](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jms/listener/DefaultMessageListenerContainer.html). – Daniel Steinmann Aug 31 '22 at 13:20
  • And finally I recommend to read [Handling poison messages in IBM MQ classes for JMS](https://www.ibm.com/docs/en/ibm-mq/9.3?topic=applications-handling-poison-messages-in-mq-classes-jms) for a robust error handling. – Daniel Steinmann Aug 31 '22 at 13:22
  • It was a migration, container managed transaction. Don't have any MQ specific config, just the bitronix TM. I'll add the code... – myborobudur Aug 31 '22 at 13:32
  • Thanks for the additional info. Since it is a Spring Boot application now, do you really need a JTA transaction manager? In order to make the MQ transactional dequeuing work correctly, Bitronix needs to be connected to MQ as well. – Daniel Steinmann Aug 31 '22 at 16:13

0 Answers0