0

I am trying to move jms messages between 2 different, remote, activeMQ brokers and after a lot of reading

I am using Atomikos, as I am writing a standalone application, and I am also using spring to get the whole thing working.

I have the following bean javaconfig setup

@Bean(name="atomikosSrcConnectionFactory")
    public AtomikosConnectionFactoryBean consumerXAConnectionFactory() {
        AtomikosConnectionFactoryBean consumerBean = new AtomikosConnectionFactoryBean();
        consumerBean.setUniqueResourceName("atomikosSrcConnectionFactory");
        consumerBean.setLocalTransactionMode(false);
        return consumerBean;
    }

    @Bean(name="atomikosDstConnectionFactory")
    public AtomikosConnectionFactoryBean producerXAConnectionFactory() {
        AtomikosConnectionFactoryBean producerBean = new AtomikosConnectionFactoryBean();
        producerBean.setUniqueResourceName("atomikosDstConnectionFactory");
        producerBean.setLocalTransactionMode(false);
        return producerBean;
    }

    @Bean(name="jtaTransactionManager")
    public JtaTransactionManager jtaTransactionManager() throws SystemException {
        JtaTransactionManager jtaTM = new JtaTransactionManager();
        jtaTM.setTransactionManager(userTransactionManager());
        jtaTM.setUserTransaction(userTransactionImp());
        return jtaTM;
    }

    @Bean(initMethod="init", destroyMethod="close", name="userTransactionManager")
    public UserTransactionManager userTransactionManager() {
        UserTransactionManager utm = new UserTransactionManager();
        utm.setForceShutdown(false);
        return utm;
    }

    @Bean(name="userTransactionImp")
    public UserTransactionImp userTransactionImp() throws SystemException {
        UserTransactionImp uti = new UserTransactionImp();
        uti.setTransactionTimeout(300);
        return uti;
    }

    @Bean(name="jmsContainer")
    @Lazy(value=true)
    public DefaultMessageListenerContainer jmsContainer() throws SystemException {
        DefaultMessageListenerContainer dmlc = new DefaultMessageListenerContainer();
        dmlc.setAutoStartup(false);
        dmlc.setTransactionManager(jtaTransactionManager());
        dmlc.setSessionTransacted(true);
        dmlc.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
        dmlc.setConnectionFactory(consumerXAConnectionFactory());
        dmlc.setDestinationName("srcQueue");
        return dmlc;
    }

    @Bean(name="transactedJmsTemplate")
    public JmsTemplate transactedJmsTemplate() {

        DynamicDestinationResolver dest = new DynamicDestinationResolver();

        JmsTemplate jmsTmp = new JmsTemplate(producerXAConnectionFactory());

        jmsTmp.setDeliveryPersistent(true);
        jmsTmp.setSessionTransacted(true);
        jmsTmp.setDestinationResolver(dest);
        jmsTmp.setPubSubDomain(false);
        jmsTmp.setReceiveTimeout(20000);
        jmsTmp.setExplicitQosEnabled(true);
        jmsTmp.setSessionTransacted(true);
        jmsTmp.setDefaultDestination(new ActiveMQQueue("destQueue"));
        jmsTmp.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);

        return jmsTmp;
    }

The 2 AtomikosConnectionFactoryBean are wrapping an ActiveMQXAConnectionFactory (One for each broker) at runtime before I start the DMLC.

I then setup a simple messageListener (which is assigned to the dmlc before it is started), with the following method:

public void onMessage(Message message) {
    final Message rcvedMsg = message;

    try{
        MessageCreator msgCreator = new MessageCreator(){
                public Message createMessage(Session session) throws JMSException{
                    Message returnMsg = null;
                    if(rcvedMsg instanceof TextMessage){
                        TextMessage txtMsg = session.createTextMessage();
                        txtMsg.setText(((TextMessage) rcvedMsg).getText());
                        returnMsg = txtMsg;
                    }
                    else if(rcvedMsg instanceof BytesMessage){
                        BytesMessage bytesMsg = session.createBytesMessage();
                        if(!(((BytesMessage) rcvedMsg).getBodyLength() > Integer.MAX_VALUE)){
                            byte[] bodyContent = new byte[(int) ((BytesMessage) rcvedMsg).getBodyLength()];
                            bytesMsg.writeBytes(bodyContent);
                            returnMsg = bytesMsg;
                        }
                    }
                    return returnMsg;
                }
            };

            jmsTemplate.send(msgCreator);
    }
    catch(JmsException | JMSException e){
        logger.error("Error when transfering message: '{}'. {}",message,e);
    }
}

The application starts without any specific errors or warnings however as soon as I put a message in the source queue I can see, through logs, that the onMessage method is being run over and over again for the same message, as if the transaction keeps being rolled back and restarted again (No errors are throw anywhere).

I have also noticed that if I happen to use the same source and destination url (Meaning the same broker but each with it's own connectionFactory), it works and messages are transfered as intended between the source and destination queue.

What I am wondering is

  1. What am I doing wrong in the setup? Why is my transaction "seemingly" being rolled back over and over again when using 2 different brokers but working when using the same (but over 2 different connection factories)?
  2. I am not completely convinced that the onMessage is currently doing proper transaction as I am currently catching all exceptions and doing nothing and I believe this will commit the transaction of the dmlc before the jmstemplate is done sending the message but I am uncertain. If this is the case, would a SessionAwareMessageListener be better instead? Should I set @Transacted in the onMessage method?

Could anybody help shine a light on the issue? All input is welcome.

UPDATE:

I realized that the issue with the "rollback" was due to the fact that both AMQs I was using were connected to each other via a network of brokers and I happened to be using the same queue name for source and destination. This led to the fact that the message was transfered by the application from one AMQ to another and then immediately, because there was a consumer on the source AMQ, the message would be transfered back to the original AMQ, which in turn was seen as a new message by the my application and transfered again and the cycle went on infinitely. The solution posted below helped with other issues.

Alexandre Thenorio
  • 2,288
  • 3
  • 31
  • 50
  • You shouldn't try/catch the exception and swallow it, this will break proper transaction management. Exceptions from the `JmsTemplate` are `JmsException`s and not `JMSException`s. Please add the code that bootstraps everything, currently we only have a part of the configuration, apparently there is more to it. – M. Deinum Feb 27 '14 at 15:09
  • Due to the fact that this is an overriden method from the original MessageListener class, I cannot simply throw the exception. As mentioned I did try using a SessionAwareMessageListener which allows me to throw the exception but that was no better. – Alexandre Thenorio Feb 27 '14 at 15:11
  • Use the `JmsUtils.convertJmsAccessException` static method to convert the exception. However why do you need to have a try/catch at all, there is nothing that throws a `JMSException`. – M. Deinum Feb 27 '14 at 15:13
  • That was a typo in my part. The real code checks for the size of the BytesMessage to make sure that it does not exceed Integer.MAX_VALUE and the getBodyLength of the BytesMessage type throws JMSException (Meaning I actually catch both types). In any case I have changed the original post to reflect the written code. I did not understand what you meant by the converJmsAccessException? – Alexandre Thenorio Feb 27 '14 at 15:23
  • Also I should add once again that I have tried using the SessionAwareMessageListener which allows me to throw a JMSException however that did not help and the issues described above was still the same – Alexandre Thenorio Feb 27 '14 at 15:25
  • The code that can throw `JMSExceptions` is inside the `MessageCreator` and is converted by the `JmsTemplate` to a `JmsException` which is a `RuntimeException`. – M. Deinum Feb 27 '14 at 15:43

1 Answers1

0
try {
   ... Code
} catch (JmsException je) {
    logger.error("Error when transfering message: '{}'. {}",message,e);
}

The code above is swallowing the exception, you should either not catch the exception or rethrow so that the transaction management can handle it appropriatly. Currently no exception is seen, a commit is performed which can lead to strange results.

I would do something like the following, JmsException is from Spring and, as most exceptions in Spring, a RuntimeException. Simply rehtrow, also to log the exception stacktrace properly remove the second {} in your log-statement.

try {
   ... Code
} catch (JmsException je) {
    logger.error("Error when transfering message: '{}'.",message,e);
    throw je;
}

However this will duplicate the logging as Spring will also log the error.

For a JMSException do something like this, converting it to a JmsException.

try {
   ... Code
} catch (JMSException je) {
    logger.error("Error when transfering message: '{}'.",message,e);
    throw JmsUtils.convertJmsAccessException(je);
}

To get more information on what happens you probably want to enable DEBUG logging for the org.springframework.jms package. This will give you insight in what happens on send/receive of the message.

Another thing you use transactional sessions and manual acknowledging of messages, however you don't do a message.acknowledge() in your code. Spring will not call it due to the JTA transaction. Try switching it to SESSION_TRANSACTED instead. At least for the DMLC.

M. Deinum
  • 115,695
  • 22
  • 220
  • 224
  • Still no go. I realised what you meant with the exceptions so I did as you wrote above and caught only JMSException and throw back a JmsException. I also added message.acknowledge (Which I have tried before) and lastly I set both DMLC and JmsTemplate to Session.SESSION_TRANSACTED. The issue is still there. Also no matter what I do, I cannot seem to get spring into debug level. Spring will write the initial INFO lines and then everything else comes from other classes than Spring – Alexandre Thenorio Feb 27 '14 at 16:34
  • OK I realised that my test rig with the 2 AMQ I was testing with had both connected through a network of brokers therefore messages were going back and forth. I have marked your answer as it was also very helpful to get the final solution in place. Thank you – Alexandre Thenorio Feb 28 '14 at 08:44
  • Glad it was useful. Could you update your question with the actual solution. This could help other people in the future in troubleshooting the problem. – M. Deinum Feb 28 '14 at 09:16
  • Done. I haven't quite tested it yet but I assume that when a JmsException is thrown, a rollback will occur and an error will appear somewhere in the console. If I add an Exception Listener to the DMLC to make nicer logs on JmsException, would that suppress a rollback and cause the message to be commited? – Alexandre Thenorio Feb 28 '14 at 09:36
  • Basically every exception leads to a rollback. You probably want to add an `ErrorHandler` instead of a `ExceptionListener`. The latter is only invoked for `JMSException`s while the first is executed for every `Throwable`. – M. Deinum Feb 28 '14 at 09:41
  • Great Thanks. Regarding the copying of message above, do I really need to be recreating the original message? Feels like a bit of unneeded work to have to copy the body over to a new message and potentialy looping through all message properties but I am uncertain what would happen if I were to try to send the original message with JmsTemplate as I dont know how spring would handle things like JmsDestination and such – Alexandre Thenorio Mar 02 '14 at 19:44
  • I guess you should be able to pass the message along. Instead of writing your own you can always look at Spring Integration. That way you would only need to configure things and don't need to program anything. All the heavy lifting will be done by Spring Integration that way. – M. Deinum Mar 02 '14 at 20:25
  • Thanks. I wasn't even aware spring had such a platform. I'll look into it. – Alexandre Thenorio Mar 03 '14 at 21:24