Hasan Tuncay
Hasan Tuncay

Reputation: 1108

Catch Exceptions inside a Message Driven Bean (MDB)

How must I handle exceptions inside a mdb? I have the funny feeling that the exception happens after the try catch block so I'm not able to catch and log it. Glassfish v3 decides to repeat the whole message. It runns into a infinite loop and writes lot's of logfiles on the harddrive.

I'm using Glassfishv3.01 + Eclipselink 2.0.1

public class SaveAdMessageDrivenBean implements MessageListener {

    @PersistenceContext(unitName="QIS") 
    private EntityManager em;

    @Resource
    private MessageDrivenContext mdc;

    public void onMessage(Message message) {
        try {
            if (message instanceof ObjectMessage) {
                ObjectMessage obj = (ObjectMessage)message;
                AnalyzerResult alyzres = (AnalyzerResult)obj.getObject();
                save(alyzres);
            }
        } catch (Throwable e) { 
            mdc.setRollbackOnly();
            log.log(Level.SEVERE, e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    private void save(AnalyzerResult alyzres) throws PrdItemNotFoundException {

       Some s = em.find(Some.class, somepk);
       s.setSomeField("newvalue");

       // SQL Exception happens after leaving this method because of missing field for ex.
    }
}    

Upvotes: 5

Views: 8207

Answers (3)

Steve C
Steve C

Reputation: 19445

I believe that the code that you have posted is mostly OK.

Your use of

    @TransactionAttribute(TransactionAttributeType.REQUIRED)

is completely ignored because this (and most other) annotations can only be applied to business methods (including onMessage). That doesn't matter though because your onMessage method gets an implicit one for free.

This leads to the fact that message handling is transactional in a Java EE container. If the transaction fails for any reason the container is required to try and deliver the message again.

Now, your code is catching the exception from the save method, which is good. But then you're explicitly marking the transaction for rollback. This has the effect of telling the container that message delivery failed and that it should try again.

Therefore, if you remove:

    mdc.setRollbackOnly();

the container will stop trying to redeliver the message.

Upvotes: 2

Carlo Pellegrini
Carlo Pellegrini

Reputation: 5686

You got a bad case of message poisoning...

The main issues I see are that:

  • you are calling directly the save() method in your onMessage(): this means thet the container has no way to inject the proper transaction handling proxy around the save method
  • in any case the save() method should have @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) in order to commit in a separate transaction, otherwise it will join the onMessage transaction (which default to REQUIRED) and bypass your exception handling code, beign committed after the successful execution of onMessage

What I woud do is:

Move the save method to a new Stateless session bean:

@Stateless
public class AnalyzerResultSaver
{
    @PersistenceContext(unitName="QIS") 
    private EntityManager em;

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    private void save(AnalyzerResult alyzres) throws PrdItemNotFoundException {
        Some s = em.find(Some.class, somepk);
        s.setSomeField("newvalue");
        // SQL Exception happens after leaving this method
    }
}

Inject this bean in your MDB:

public class SaveAdMessageDrivenBean implements MessageListener {

    @Inject  
    private AnalyzerResultSaver saver;

    @Resource
    private MessageDrivenContext mdc;

    public void onMessage(Message message) {
        try {
            if (message instanceof ObjectMessage) {
                ObjectMessage obj = (ObjectMessage)message;
                AnalyzerResult alyzres = (AnalyzerResult)obj.getObject();
                saver.save(alyzres);
            }
        } catch (Throwable e) { 
            mdc.setRollbackOnly();
            log.log(Level.SEVERE, e);
        }
    }
}

Another tip: in this code the message poisoning still exists. Now it derives from the line invoking mdc.setRollbackOnly();.

I'd suggest here to log the exception and transfer the message to a poison queue, thus preventing the container to resubmit the message ad infinitum.

UPDATE:

A 'poison queue' or 'error queue' is simply a mean to guarantee that your (hopefully recoverable) discarded messages will not be completely lost. It is used heavily in integration scenarios, where the correctness of the message data is not guaranteed.

Setting up a poison queue implies defining a destination queue or topic and redeliver the 'bad' messages to this destination.

Periodically, an operator should inspect this queue (via a dedicated application) and either modify the messages and resubmit to the 'good' queue, or discard the message and ask for a resumbit.

Upvotes: 5

Fritz
Fritz

Reputation: 10055

If I'm not mistaken, you're letting the container handle the transactions. This way, the entity manager will queue the operations that will be flushed after the method finishes, that's why you're having exceptions after the method is finished.

Using em.flush() directly as a final step in the method will execute all the related queries of the transaction, throwing the exceptions there instead of being thrown later when the flush() is made by the container while commiting the transaction.

Upvotes: 1

Related Questions