Search code examples
springspring-bootactivemq-classicjmstemplate

Is it possible to enforce message order on ActiveMQ topics using Spring Boot and JmsTemplate?


In playing around with Spring Boot, ActiveMQ, and JmsTemplate, I noticed that it appears that message order is not always preserved. In reading on ActiveMQ, "Message Groups" are offered as a potential solution to preserving message order when sending to a topic. Is there a way to do this with JmsTemplate?

Add Note: I'm starting to think that JmsTemplate is nice for "getting launched", but has too many issues.

Sample code and console output posted below...

@RestController
public class EmptyControllerSB {

    @Autowired
    MsgSender msgSender;

    @RequestMapping(method = RequestMethod.GET, value = { "/v1/msgqueue" })
    public String getAccount() {
        msgSender.sendJmsMessageA();
        msgSender.sendJmsMessageB();
        return "Do nothing...successfully!";
    }
}

@Component
public class MsgSender {

    @Autowired
    JmsTemplate jmsTemplate;

    void sendJmsMessageA() {
        jmsTemplate.convertAndSend(new ActiveMQTopic("VirtualTopic.TEST-TOPIC"), "message A");
    }

    void sendJmsMessageB() {
        jmsTemplate.convertAndSend(new ActiveMQTopic("VirtualTopic.TEST-TOPIC"), "message B");
    }
}

@Component
public class MsgReceiver {

    private final String consumerOne = "Consumer.myConsumer1.VirtualTopic.TEST-TOPIC";
    private final String consumerTwo = "Consumer.myConsumer2.VirtualTopic.TEST-TOPIC";

    @JmsListener(destination = consumerOne )
    public void receiveMessage1(String strMessage) {
        System.out.println("Received on #1a -> " + strMessage);
    }

    @JmsListener(destination = consumerOne )
    public void receiveMessage2(String strMessage) {
        System.out.println("Received on #1b -> " + strMessage);
    }

    @JmsListener(destination = consumerTwo )
    public void receiveMessage3(String strMessage) {
        System.out.println("Received on #2 -> " + strMessage);
    }
}

Here's the console output (note the order of output in first sequence)...

\Intel\Intel(R) Management Engine Components\DAL;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files (x86)\gnupg\bin;C:\Users\LesR\AppData\Local\Microsoft\WindowsApps;c:\Gradle\gradle-5.0\bin;;C:\Program Files\JetBrains\IntelliJ IDEA 2018.3\bin;;.]
2019-04-03 09:23:08.408  INFO 13936 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2019-04-03 09:23:08.408  INFO 13936 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 672 ms
2019-04-03 09:23:08.705  INFO 13936 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-04-03 09:23:08.845  INFO 13936 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-04-03 09:23:08.877  INFO 13936 --- [           main] mil.navy.msgqueue.MsgqueueApplication    : Started MsgqueueApplication in 1.391 seconds (JVM running for 1.857)
2019-04-03 09:23:14.949  INFO 13936 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-04-03 09:23:14.949  INFO 13936 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2019-04-03 09:23:14.952  INFO 13936 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 3 ms
Received on #2 -> message A
Received on #1a -> message B
Received on #1b -> message A
Received on #2 -> message B

<HIT DO-NOTHING ENDPOINT AGAIN>
Received on #1b -> message A
Received on #2 -> message A
Received on #1a -> message B
Received on #2 -> message B

Solution

  • BLUF - Add "?consumer.exclusive=true" to the declaration of the destination for the JmsListener annotation.

    It seems that the solution is not that complex, especially if one abandons ActiveMQ's "message groups" in favor or "exclusive consumers". The drawback to the "message groups" is that the sender has to have prior knowledge of the potential partitioning of message consumers. If the producer has this knowledge, then "message groups" are a nice solution, as the solution is somewhat independent of the consumer.

    But, a similar solution can be implemented from the consumer side, by having the consumer declare "exclusive consumer" on the queue. While I did not see anything in the JmsTemplate implementation that directly supports this, it seems that Spring's JmsTemplate implementation passes the queue name to ActiveMQ, and then ActiveMQ "does the right thing" and enforces the exclusive consumer behavior.

    So...

    Change the following...

    private final String consumerOne = "Consumer.myConsumer1.VirtualTopic.TEST-TOPIC";
    

    to...

    private final String consumerOne = "Consumer.myConsumer1.VirtualTopic.TEST-TOPIC";?consumer.exclusive=true
    

    Once I did this, only one of the two declared receive methods were invoked, and message order was maintained in all my test runs.