3

I am trying to set up Dead Letter Queue monitoring for a system. So far, I can get it to be thrown in the DLQ queue without problems when the message consumption fails on the consumer. Now I'm having some trouble with getting the reason why it failed;

currently I get the following

java.lang.Throwable: Delivery[2] exceeds redelivery policy imit:RedeliveryPolicy 
  {destination = queue://*, 
   collisionAvoidanceFactor = 0.15, 
   maximumRedeliveries = 1, 
   maximumRedeliveryDelay = -1, 
   initialRedeliveryDelay = 10000, 
   useCollisionAvoidance = false, 
   useExponentialBackOff = true, 
   backOffMultiplier = 5.0, 
   redeliveryDelay = 10000, 
   preDispatchCheck = true}, 
   cause:null

I do not know why cause is coming back as null. I'm using Spring with ActiveMQ. I'm using the DefaultJmsListenerContainerFactory, which creates a DefaultMessageListenerContainer. I would like cause to be filled with the exception that happened on my consumer but I can't get it to work. Apparently there's something on Spring that's not bubbling up the exception correctly, but I'm not sure what it is. I'm using spring-jms:4.3.10. I would really appreciate the help.

CryptoFool
  • 21,719
  • 5
  • 26
  • 44
lfernandez93
  • 143
  • 1
  • 1
  • 7

1 Answers1

1

I am using spring-boot-starter-activemq:2.2.2.RELEASE (spring-jms:5.2.2, activemq-client-5.15.11) and I have the same behavior.

(links point to the versions I use)

The rollback cause is added here for the POSION_ACK_TYPE (sic!). Its assignment to the MessageDispatch is only happening in one place: when dealing with a RuntimeException in the case there is a javax.jms.MessageListener registered.

Unfortunately (for this particular case), Spring doesn't register one, because it prefers to deal with its own hierarchy. So, long story short, there is no chance to make it happen with Spring out-of-the-box.

However, I managed to write an hack-ish way of getting an access to the MessageDispatch instance dealt with, inject the exception as the rollback cause, and it works!

package com.example;

import org.springframework.jms.listener.DefaultMessageListenerContainer;

import javax.jms.*;

public class MyJmsMessageListenerContainer extends DefaultMessageListenerContainer {

    private final MessageDeliveryFailureCauseEnricher messageDeliveryFailureCauseEnricher = new MessageDeliveryFailureCauseEnricher();

    private MessageConsumer messageConsumer; // Keep for later introspection

    @Override
    protected MessageConsumer createConsumer(Session session, Destination destination) throws JMSException {
        this.messageConsumer = super.createConsumer(session, destination);
        return this.messageConsumer;
    }

    @Override
    protected void invokeListener(Session session, Message message) throws JMSException {
        try {
            super.invokeListener(session, message);
        } catch (Throwable throwable) {
            messageDeliveryFailureCauseEnricher.enrich(throwable, this.messageConsumer);
            throw throwable;
        }
    }
}

Note: don't deal with the Throwable by overriding the protected void handleListenerException(Throwable ex) method, because at that moment some cleanup already happened in the ActiveMQMessageConsumer instance.

package com.example;

import org.apache.activemq.ActiveMQMessageConsumer;
import org.apache.activemq.command.MessageDispatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;

import javax.jms.MessageConsumer;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

class MessageDeliveryFailureCauseEnricher {
    private static final Logger logger = LoggerFactory.getLogger(MessageDeliveryFailureCauseEnricher.class);

    private final Map<Class<?>, Field> accessorFields = new HashMap<>();
    private final Field targetField;

    public MessageDeliveryFailureCauseEnricher() {
        this.targetField = register(ActiveMQMessageConsumer.class, "deliveredMessages");
        // Your mileage may vary; here is mine:
        register("brave.jms.TracingMessageConsumer", "delegate");
        register("org.springframework.jms.connection.CachedMessageConsumer", "target");
    }

    private Field register(String className, String fieldName) {
        Field result = null;
        if (className == null) {
            logger.warn("Can't register a field from a missing class name");
        } else {
            try {
                Class<?> clazz = Class.forName(className);
                result = register(clazz, fieldName);
            } catch (ClassNotFoundException e) {
                logger.warn("Class not found on classpath: {}", className);
            }
        }
        return result;
    }

    private Field register(Class<?> clazz, String fieldName) {
        Field result = null;
        if (fieldName == null) {
            logger.warn("Can't register a missing class field name");
        } else {
            Field field = ReflectionUtils.findField(clazz, fieldName);
            if (field != null) {
                ReflectionUtils.makeAccessible(field);
                accessorFields.put(clazz, field);
            }
            result = field;
        }
        return result;
    }

    void enrich(Throwable throwable, MessageConsumer messageConsumer) {
        if (throwable != null) {
            if (messageConsumer == null) {
                logger.error("Can't enrich the MessageDispatch with rollback cause '{}' if no MessageConsumer is provided", throwable.getMessage());
            } else {
                LinkedList<MessageDispatch> deliveredMessages = lookupFrom(messageConsumer);
                if (deliveredMessages != null && !deliveredMessages.isEmpty()) {
                    deliveredMessages.getLast().setRollbackCause(throwable); // Might cause problems if we prefetch more than 1 message
                }
            }
        }
    }

    private LinkedList<MessageDispatch> lookupFrom(Object object) {
        LinkedList<MessageDispatch> result = null;
        if (object != null) {
            Field field = accessorFields.get(object.getClass());
            if (field != null) {
                Object fieldValue = ReflectionUtils.getField(field, object);
                if (fieldValue != null) {
                    if (targetField == field) {
                        result = (LinkedList<MessageDispatch>) fieldValue;
                    } else {
                        result = lookupFrom(fieldValue);
                    }
                }
            }
        }

        return result;
    }
}

The magic happen in the second class:

  1. At construction time we make some private fields accessible.
  2. When a Throwable is caught, we traverse these fields to end up with the appropriate MessageDispatch instance (beware if you prefetch more than 1 message), and inject it the throwable we want to be part of the dlqDeliveryFailureCause JMS property.

I crafted this solution this afternoon, after hours of debugging (thanks OSS!) and many trials and errors. It works, but I have the feeling it's more of an hack than a real, solid solution. With that in mind, I made my best to avoid side effects, so the worst that can happen is no trace of the original Throwable in the message ending in the Dead Letter Queue.

If I missed the point somewhere, I'b be glad to learn more about this.

Dimitri Hautot
  • 438
  • 5
  • 12