Although robust and successful, this transactional message consumption model has some serious drawbacks. First of all, distributed transactions add a significant performance impact to the processing (it is not unusual to see a 50 percent performance degradation when switching from local transaction to XA).
A second disadvantage is that because of CMT, the actual transaction commit happens outside of application code, after the return from the
onMessage() call. This may not seem like a big deal (after all, the whole idea of CMT is to relieve the application from handling transactions), but there are some unpleasant implications—some error conditions won't be detected until the transactions commit. For example, in BEA WebLogic Server, by default, all DML operations resulting from manipulations with CMP beans (create, update, and so on) are deferred until transaction commit time. This means that an application can think it successfully updated an instance of a CMP bean, while in fact the actual SQL update would fail because of some constraint violation in the database. The worst part is that application code won't be able to react to this or just log it properly because it would never see that exception.
Although there is a workaround for delayed DML operations—for example, in BEA WebLogic Server it can be disabled in the deployment descriptor—it comes with a performance penalty. The J2EE server will no longer be able to aggregate and/or batch SQL updates for more efficient execution, or skip them altogether if the transaction is later marked for rollback later.
This article proposes that embracing a bean-managed transaction (BMT) approach can provide the same quality of service, with far more control over transaction life cycle. Application code would have the opportunity to recover and/or report errors much more clearly, while avoiding all disadvantages of the CMT model described above. Moreover, we expect a significant performance gain as a result of removing message retrieval from the transaction scope.
Before we look at the BMT approach, we need to analyze what would happen with message consumption from the queue in this case. If we deployed the MDB with BMT demarcation, the J2EE server would no longer enlist the JMS destination (queue or topic) that the MDB is listening on into the transaction (the transaction will be started after the message is picked from the queue). In this case, the BMT MDB should be configured with a non-XA connection factory in the deployment descriptor; otherwise the J2EE server will fail to deploy it.
According to the JMS specification (JMS 1.1 section 4.5.2), if a message listener is deployed non-transitionally with
DUPS_OK_ACKNOWLEDGE modes and
RuntimeException or any subclass of it is thrown from the
onMessage() method, then the message will be redelivered. In other words, it is possible to redesign our use case to use BMT, and if something goes wrong during message processing, the application code can throw a
RuntimeException, and the message would be redelivered (retried). This approach works pretty well, because it's just natural to use
RuntimeException to indicate unrecoverable errors (for example the Spring Framework's exception hierarchy is almost entirely based on subclasses of
RuntimeException). The message will be redelivered up to a certain number of times (configurable at the MOM software level) after which it's usually discarded or moved into a dead message queue, or alternatively the application code could count the number of times the message was redelivered and decide when it's time to stop trying to process and either consume it without processing (generating an error message if appropriate) or move it into the separate queue.
As you can see, the behavior described above guarantees that we have control on message redelivery, and it's possible for the application to retry in case of repeated processing failures. We are going to show how we can guarantee once-and-only-once behavior in the BMT case.
Let's now look at the sequence of events in a non-transaction message consumption model:
onMessage()method of the MDB.
onMessage()call returns successfully (without a
RuntimeExceptionbeing thrown), then the message will be acknowledged and removed from the queue.
You probably notice that logically, the sequence is somewhat simpler, although the implementation could be a little bit more complicated than the plain CMT case. The payback here is significant flexibility in handling message processing. For example it's up to the application code to either commit or roll back the transaction, and it's independent from message acknowledgment. Note that the message is not acknowledged until the successful return from the
onMessage() method call.
So far so good, but let's see if we can guarantee the same QoS (once and only once) as when using the transactional message consumption model. We need to analyze two things:
The first item in the list above is very simple. As evident from our discussion, since a message is not acknowledged until after a successful return from the
onMessage() we just need to make sure that our application code is not swallowing exceptions where it shouldn't and that we're rolling back transactions in the case of any exception thrown. This is a pretty natural approach.
The second item in the list above is just a bit trickier. As we have seen, if an exception is thrown during business processing, the message will be redelivered. It's not a problem if we are following the paradigm described above: If there is any exception, roll back the transaction and re-throw the exception further, to the J2EE server. Of course, action should be taken to prevent infinite message delivery if the error is truly unrecoverable. This could be done at the MOM level (for example, dead message queue, redelivery limit) or, as we are going to show, this could be handled on application level.
In the case of a previous processing attempt terminated with a transaction rollback, message redelivery is not a problem. From the application perspective, the next delivery attempt should be treated as a new message (the result of the previous processing attempt of the same message is not visible because the transaction was rolled back), so we are fine in this case without any special processing rules.
It's important to analyze as well what would happen in case of catastrophic failures of different components—for example if the J2EE server crashed—is the system going to be in consistent state, from our required QoS point of view, after restart? Any failure before message acknowledgment (which happens on the very last step) is going to result in message redelivery. Therefore, we need to take care of only one failure scenario, namely when the J2EE server, or MOM, or connection between them fail after our code successfully finished processing and committed the BMT transaction but before the message was acknowledged by the J2EE server. This would leave the system in an inconsistent state: From the application point of view the message was successfully processed, but from the MOM point of view the message hasn't been successfully delivered (it was not acknowledged because of the failure). This could result in duplicate message delivery and processing, violating our QoS contract. Arguably, since the time interval between transaction commit and acknowledgment should be very short, this failure scenario should not happen very often. Nevertheless if your system is under heavy load, then there could potentially be dozens if not hundreds of messages processed simultaneously, and we should take this into account when building our recovery mechanism.
It's also important to emphasize once again that we are concentrating on the most strict QoS level (once and only once), and therefore recovery from duplicates in such rare cases are needed. If your use case could be developed with more relaxed QoS levels (for example, once or more), this recovery mechanism we are about to describe could be skipped.
To summarize, in order to ensure a once-and-only-once QoS while using a non-transactional message retrieval with BMT, we need to have control over the message redelivery behavior (which is achieved by throwing a
RuntimeException from the
onMessage() method as per the EJB specification) and some mechanism to prevent processing duplicate messages in the event of failures during certain stages of message processing (between the return from the
onMessage() call and the message acknowledgment). We're going to discuss prevention of processing duplicate messages next.