I have a JMSReceiver class which is listening on a MQ Queue. This class implements the MessageListener interface. I wish to implement the logic to retry a message for specified number of times by getting the message to rollback. To do so I have to catch the business exception and wrap it in a RuntimeException so that message gets rolled back to the MQ and gets replayed. i wish to implement this in better way.
Current Implementation
class JMSReceiver implements MessageListener{
public void onMessage(Message msg){
logger.info("**********Message received in consumer");
try {
//Do some business which throws a business exception
} catch (Exception e) {
try {
logger.info("####Redelivery count"+msg.getIntProperty("JMSXDeliveryCount"));
if(msg.getIntProperty("JMSXDeliveryCount")<10){
logger.info("####MQ ISSUE: Redelivery attempted for message. Redelivery attempt: "+msg.getIntProperty("JMSXDeliveryCount"));
throw new RuntimeException("Redelivery Attempted"+e.getMessage());
}else{
logger.info("####MQ ISSUE: Redelivery attempts exhausted for message");
}
} catch (JMSException e1) {
e1.printStackTrace();
logger.info("####MQ ISSUE: Exception occured while getting JMSXDeliveryCount");
}
}
}
Expected
The above implementation works. It rollsback the message to MQ and the redelivery count increases. I even tried doing session.rollback() but when I do that the redelivery count does not increase and I can replay the message. Please advise a better way to implement this ?
You can create JMS session with CLIENT_ACKNOWLEDGE as message acknowledge mode. And then in the onMessage() method, do not call msg.Acknowledge(). Not calling msg.Acknowledge() will ensure the same is delivered again.
session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
Note that calling Acknowledge() on one message will acknowledge all messages received since the last time the method was called.
Update
Session creation
connection = cf.createConnection("user","password");
System.out.println("Connection created.");
session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
System.out.println("Session created.");
OnMessage() method - Message is being acknowledged on 6th attempt.
consumer.setMessageListener(new MessageListener() {
public void onMessage(Message msg) {
try {
// Display the message that just arrived
System.out.println(msg);
if(msg.getIntProperty("JMSXDeliveryCount") > 5){
msg.acknowledge();
}
} // end try
catch (Exception e) {
System.out.println("Exception caught in onMessage():\n" + e);
}
return;
} // end onMessage()
}); // end setMessageListener
Related
I have deployed my Java-MDB based application using ActiveMQ as messaging service . I could see that a few messages have been in pending status for quite some time on some queues. I have read that this happens when ActiveMQ delivers the message and consumer consumes the message but doesn't send the ack back. But I could not see any related loggers on the consumer/application side which proves that the message is consumed.
Could anyone please help me understand the reason of message being stuck in pending state.
Edit - Adding the details:
We are using Auto-acknowledge as acknowledgeMode and below is the onMessage method used on consumer side.
public void onMessage(Message message) {
try {
// Clear all ThreadLocal in SQLQueryHelper.
SQLQueryHelper.clearCache();
String messageOut = processMessage(message);
// if there is a reply, send it out
if (messageOut != null) {
logger.warn(LoggerKeys.LOG_1_ARGS,
new String[] {"Reply from MDB not supported. " + messageOut});
}
} catch (Throwable e) {
logger.error(LoggerKeys.LOG_1_ARGS,
new String[] {"Error encountered: " + e.toString()});
try {
//put message on error queue
handleError(message, e);
} catch (Throwable e2) {
//retry to put message on error queue
handleErrorAndRollBack(message, e2);
}
}
}
I encountered a knotty problem when receiving message from WildFly JMS queue. My code is below:
Session produceSession = connectionFactory.createConnection().createSession(false, Session
.CLIENT_ACKNOWLEDGE);
Session consumerSession = connectionFactory.createConnection().createSession(false, Session
.CLIENT_ACKNOWLEDGE);
ApsSchedule apsSchedule = new ApsSchedule();
boolean success;
MessageProducer messageProducer = produceSession.createProducer(outQueueMaxusOrder);
success = apsSchedule.sendD90Order(produceSession,messageProducer, d90OrderAps);
if (!success) {
logger.error("Can't send APS schedule msg ");
} else {
MessageConsumer consumer = consumerSession.createConsumer(inQueueDeliveryDate);
data = apsSchedule.receiveD90Result(consumerSession,consumer);
}
then getting into the receiveD90Result():
public DeliveryData receiveD90Result(Session session, MessageConsumer consumer) {
DeliveryData data = null;
try {
Message message = consumer.receive(10000);
if (message == null) {
return null;
}
TextMessage msg = (TextMessage) message;
String text = msg.getText();
logger.debug("Receive APS d90 result: {}", text);
ObjectMapper mapper = new ObjectMapper();
data = mapper.readValue(text, DeliveryData.class);
} catch (JMSException je) {
logger.error("Can't receive APS d90 order result: {}", je.getMessage());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
consumer.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
return data;
}
But when implementing the consumer.receive(10000), the project can't get a message from queue. If I use asynchronous way of MDB to listen the queue, I can get the message from queue. How to resolve it?
There are multiple modes you can choose to get a message from the queue. Message Queues are by default asynchronous in usage. There are however cases when you want to read it synchronously , for example sending a message with account number and using another queue to read the response and match it with a message id or a message correlation id. When you do a receive , the program is waiting for a message to arrive within that polling interval specified in receive.
The code snippet you have , as i see it uses the psuedo synchronous approach. If you have to use it as an MDB , you will have to implement message driven bean (EJB Resource) or message listener.
The way that MDB/Message Listener works is more event based , instead of a poll with a timeout (like the receive) , you implement a callback called onMessage() that is invoked every time there is a message. Instead of a synchronous call , this becomes asynchronous. Your application may require some changes both in terms of design.
I don't see where you're calling javax.jms.Connection.start(). In fact, it doesn't look like you even have a reference to the javax.jms.Connection instance used for your javax.jms.MessageConsumer. If you don't have a reference to the javax.jms.Connection then you can't invoke start() and you can't invoke close() when you're done so you'll be leaking connections.
Furthermore, connections are "heavy" objects and are meant to be re-used. You should create a single connection for both the producer and consumer. Also, if your application is not going to use the javax.jms.Session from multiple threads then you don't need multiple sessions either.
I have a requirement where I have to add and update message header in case of message retry.
Here is my listener or consumer. My message is getting retried but I am getting Exception when setting the header. Please advise the correct way of doing this.
As per JMS spec and it says that Message Headers are never read-only.
javax.jms.MessageNotWriteableException: Message properties are read-only
public void onMessage(Message message) {
if (message != null && message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
String input = textMessage.getText();
throw new Exception();
} catch (Throwable t) {
t.printStackTrace();
try {
message.setStringProperty("retryable","YES");
} catch (JMSException e) {
e.printStackTrace();
}
throw new RuntimeException(t);
}
}
}
What you are trying won't work for a number of reasons. You are attempting to set a message PROPERTY on an incoming message which will indeed be read-only. The message you receive is a copy of the actual message and not the one that would be resent if inside a TX and was eligible for redelivery.
To do any sort of update to a delivered message that encounters an error during its processing you need to create a new instance and decorate it with the appropriate information and then place it back on the Destination using a MessageProducer.
Here's what I know so far (please correct me):
In the RabbitMQ Java client, operations on a channel throw IOException when there is a general network failure (malformed data from broker, authentication failures, missed heartbeats).
Operations on a channel can also throw the ShutdownSignalException unchecked exception, typically an AlreadyClosedException when we tried to perform an action on the channel/connection after it has been shut down.
The shutting down process happens in the event of "network failure, internal failure or explicit local shutdown" (e.g. via channel.close() or connection.close()). The shutdown event propagates down the "topology", from Connection -> Channel -> Consumer, and when the Channel it calls the Consumer's handleShutdown() method gets called.
A user can also add a shutdown listener which is called after the shutdown process completes.
Here is what I'm missing:
Since an IOException indicates a network failure, does it also initiate a shutdown request?
How does using auto-recovery mode affect shutdown requests? Does it cause channel operations to block while it tries to reconnect to the channel, or will the ShutdownSignalException still be thrown?
Here is how I'm handling exceptions at the moment, is this a sensible approach?
My setup is that I'm polling a QueueingConsumer and dispatching tasks to a worker pool. The rabbitmq client is encapsulated in MyRabbitMQWrapper here. When an exception occurs polling the queue I just gracefully shutdown everything and restart the client. When an exception occurs in the worker I also just log it and finish the worker.
My biggest worry (related to Question 1): Suppose an IOException occurs in the worker, then the task doesn't get acked. If the shutdown does not then occur, I now have an un-acked task that will be in limbo forever.
Pseudo-code:
class Main {
public static void main(String[] args) {
while(true) {
run();
//Easy way to restart the client, the connection has been
//closed so RabbitMQ will re-queue any un-acked tasks.
log.info("Shutdown occurred, restarting in 5 seconds");
Thread.sleep(5000);
}
}
public void run() {
MyRabbitMQWrapper rw = new MyRabbitMQWrapper("localhost");
try {
rw.connect();
while(!Thread.currentThread().isInterrupted()) {
try {
//Wait for a message on the QueueingConsumer
MyMessage t = rw.getNextMessage();
workerPool.submit(new MyTaskRunnable(rw, t));
} catch (InterruptedException | IOException | ShutdownSignalException e) {
//Handle all AMQP library exceptions by cleaning up and returning
log.warn("Shutting down", e);
workerPool.shutdown();
break;
}
}
} catch (IOException e) {
log.error("Could not connect to broker", e);
} finally {
try {
rw.close();
} catch(IOException e) {
log.info("Could not close connection");
}
}
}
}
class MyTaskRunnable implements Runnable {
....
public void run() {
doStuff();
try {
rw.ack(...);
} catch (IOException | ShutdownSignalException e) {
log.warn("Could not ack task");
}
}
}
In the JMS API doc, it said:
public Message receive() throws JMSException
Receives the next message
produced for this message consumer. This call blocks indefinitely
until a message is produced or until this message consumer is closed.
If this receive is done within a transaction, the consumer retains the message until the transaction commits.
Here I have three questions:
1. in the code, do we need while-loop to receive message ? like:
while(true){
Message msg = queue.receive();
....
}
what is the transaction setting ? how to commit a transaction ? like this:
boolean transacted = false;
session = connection.createQueueSession(transacted, Session.AUTO_ACKNOWLEDGE);
receiveNoWait() has transaction support ? how to use it ?
Thanks
If you are going to use receive then you will need some sort of loop to keep receiving messages after the first one is received. Remember that you can also setup a messagelistener and get the received messages async via a callback method and not have to block.
The transaction is generally set to AUTO_ACKNOWLEDGE by default which means that as soon as the message is taken from the queue it is gone and cannot be rolled back. If you want to setup a transaction you need to set the session to transacted and the method to SESSION_TRANSACTED. When you call commit() on the session the messages will be acknowledged on the queue.
receiveNoWait() can have transaction support if you setup the acknowledgement mode correctly and you use commit() and rollback() on the session.
If I were you I would create a MessageListener and not have to worry about spinning a thread to poll the receive methods. Keep in mind that an implicit transaction is started once the session is created.
public class JmsAdapter implements MessageListener, ExceptionListener
{
private ConnectionFactory connFactory = null;
private Connection conn = null;
private Session session = null;
public void receiveMessages()
{
try
{
this.session = this.conn.createSession(true, Session.SESSION_TRANSACTED);
this.conn.setExceptionListener(this);
Destination destination = this.session.createQueue("SOME_QUEUE_NAME");
this.consumer = this.session.createConsumer(destination);
this.consumer.setMessageListener(this);
this.conn.start();
}
catch (JMSException e)
{
//Handle JMS Exceptions Here
}
}
#Override
public void onMessage(Message message)
{
try
{
//Do Message Processing Here
//Message sucessfully processed... Go ahead and commit the transaction.
this.session.commit();
}
catch(SomeApplicationException e)
{
//Message processing failed.
//Do whatever you need to do here for the exception.
//NOTE: You may need to check the redelivery count of this message first
//and just commit it after it fails a predefined number of times (Make sure you
//store it somewhere if you don't want to lose it). This way you're process isn't
//handling the same failed message over and over again.
this.session.rollback()
}
}
}