0

1. Context:

A two-modules/microservice application developed with SpringBoot 2.3.0 and ActiveMQ. Also we use ActiveMQ 5.15.13 server/broker. Broker is defined in both modules with application properties. Also broker connection pool is defined in both modules as well with application properties and added in both modules the pooled-jms artifact dependency (with maven):

spring.activemq.broker-url=xxx
spring.activemq.user=xxx
spring.activemq.password=xx
spring.activemq.non-blocking-redelivery=true
spring.activemq.pool.enabled=true
spring.activemq.pool.time-between-expiration-check=5s
spring.activemq.pool.max-connections=10
spring.activemq.pool.max-sessions-per-connection=10
spring.activemq.pool.idle-timeout=60s

Other configurations for JMS I done are:

spring.jms.listener.acknowledge-mode=auto
spring.jms.listener.auto-startup=true
spring.jms.listener.concurrency=5
spring.jms.listener.max-concurrency=10
spring.jms.pub-sub-domain=false
spring.jms.template.priority=100
spring.jms.template.qos-enabled=true
spring.jms.template.delivery-mode=persistent

In module 1 the JmsTemplate is used to send synchronous messages (or we can name replay-messages as well). I've opted out for a proper queue instead of a temporary queue as I understand that if there are lots of messages sent than a temporary queue is not recommended to be used for replays - so that's what I did.

2. Code samples:

MODULE 1:

@Value("${app.request-video.jms.queue.name}")
private String requestVideoQueueNameAppProperty;
@Bean
public Queue requestVideoJmsQueue() {
    logger.info("Initializing requestVideoJmsQueue using application property value for " +
            "app.request-video.jms.queue.name=" + requestVideoQueueNameAppProperty);
    return new ActiveMQQueue(requestVideoQueueNameAppProperty);
}

@Value("${app.request-video-replay.jms.queue.name}")
private String requestVideoReplayQueueNameAppProperty;
@Bean
public Queue requestVideoReplayJmsQueue() {
    logger.info("Initializing requestVideoReplayJmsQueue using application property value for " +
            "app.request-video-replay.jms.queue.name=" + requestVideoReplayQueueNameAppProperty);
    return new ActiveMQQueue(requestVideoReplayQueueNameAppProperty);
}

@Autowired
private JmsTemplate jmsTemplate;

public Message callSendAndReceive(TextJMSMessageDTO messageDTO, Destination jmsDestination, Destination jmsReplay) {

return jmsTemplate.sendAndReceive(jmsDestination, jmsSession -> {
        try {
            TextMessage textMessage = jmsSession.createTextMessage();
            textMessage.setText(messageDTO.getText());
            textMessage.setJMSReplyTo(jmsReplay);
            textMessage.setJMSCorrelationID(UUID.randomUUID().toString());
            textMessage.setJMSDeliveryMode(DeliveryMode.NON_PERSISTENT);
            return textMessage;
        } catch (IOException e) {
            logger.error("Error sending JMS message to destination: " + jmsDestination, e);
            throw new JMSException("Error sending JMS message to destination: " + jmsDestination);
        }
    });
}

MODULE 2:

@JmsListener(destination = "${app.backend-get-request-video.jms.queue.name}")

public void onBackendGetRequestsVideoMessage(TextMessage message, Session session) throws JMSException, IOException {
    logger.info("Get requests video file message consumed!");
    try {
        Object replayObject = handleReplayAction(message);
        JMSMessageDTO messageDTO = messageDTOFactory.getJMSMessageDTO(replayObject);
        Message replayMessage = messageFactory.getJMSMessage(messageDTO, session);

        BytesMessage replayBytesMessage = jmsSession.createBytesMessage();
        fillByteMessageFromMediaDTO(replayBytesMessage, mediaMessageDTO);
        replayBytesMessage.setJMSCorrelationID(message.getJMSCorrelationID());
        final MessageProducer producer = session.createProducer(message.getJMSReplyTo());
        producer.send(replayBytesMessage);
        JmsUtils.closeMessageProducer(producer);
    } catch (JMSException | IOException e) {
        logger.error("onBackendGetRequestsVideoMessage()JMSException: " + e.getMessage(), e);
        throw e;
    }
}

private void fillByteMessageFromMediaDTO(BytesMessage bytesMessage, MediaJMSMessageDTO mediaMessageDTO)
        throws IOException, JMSException {
    String filePath = fileStorageConfiguration.getMediaFilePath(mediaMessageDTO);
    FileInputStream fileInputStream = null;
    try (FileInputStream fileInputStream = new FileInputStream(filePath)) {
        byte[] byteBuffer = new byte[1024];
        int bytes_read = 0;
        while ((bytes_read = fileInputStream.read(byteBuffer)) != -1) {
            bytesMessage.writeBytes(byteBuffer, 0, bytes_read);
        }
    } catch (JMSException e) {
        logger.error("Can not write data in JMS ByteMessage from file: " + fileName, e);
    } catch (FileNotFoundException e) {
        logger.error("Can not open stream to file: " + fileName, e);
    } catch (IOException e) {
        logger.error("Can not read data from file: " + fileName, e);
    }
}

3. The problem:

As I send many messages and receive many corresponding replays through producer/comsumer/JmsTamplate both application modules 1 and 2 are fast-filling the heap memory allocated until an out-of-memory error is thrown, but the memory leak appears only when using synchronous messages with replay as shown above.

I've debugged my code and all instances (session, producers, consumers, jmsTamplate, etc) are pooled and have instances of the right classes from pooled-jms library; so pool should - apparently - work properly. I've made a heap dump of the second module and looks like producers messages (ActiveMQBytesMessage) are still in memory even long time after have been successfully consumed by the right consumer.

I have asynchronous messages sent as well in my modules and seams that those messages producer-consumer works well; the problem is present only for the synch/replay messages producer-consumer.

Sample heap dump files - taken after full night of application inactivity - as following:

  1. module 1 module_1_dump
  2. module 2 module_2_dump
  3. activemq broker/server activemq_dump

Anyone have any idea what I'm doing wrong?!

  • If you have a heap dump you ought to be able to see what is holding onto the Message instances, otherwise without knowing where they are being held it's anyone's guess. – Tim Bish Jun 21 '20 at 15:54
  • @TimBish Added heap dump files. Still some memory leaks even I've managed to solve the producers messages leaks which I do not see in memory dumps anymore - so this is good. – stefanello_cs Jun 22 '20 at 10:05

0 Answers0