org.wso2.andes.kernel.MessageHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.wso2.andes.kernel.MessageHandler.java

Source

/*
 * Copyright (c) 2016, WSO2 Inc. (http://wso2.com) All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.wso2.andes.kernel;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.andes.configuration.AndesConfigurationManager;
import org.wso2.andes.configuration.enums.AndesConfiguration;
import org.wso2.andes.kernel.slot.Slot;
import org.wso2.andes.kernel.slot.SlotDeletionExecutor;
import org.wso2.andes.kernel.slot.SlotDeliveryWorkerManager;
import org.wso2.andes.kernel.slot.SlotReAssignTask;
import org.wso2.andes.kernel.subscription.StorageQueue;
import org.wso2.andes.server.queue.DLCQueueUtils;
import org.wso2.andes.tools.utils.MessageTracer;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
 * This class is for message handling operations of a queue. Handling
 * Message caching, buffering, keep trace of messages etc
 */
public class MessageHandler {

    private static Log log = LogFactory.getLog(MessageHandler.class);

    /**
     * Reference to MessageStore. This is the persistent storage for messages
     */
    private MessageStore messageStore;

    /**
     * Manager for message delivery to subscriptions.
     */
    private SlotDeliveryWorkerManager messageDeliveryManager;

    /**
     * Name of the queue to handle
     */
    private String queueName;

    /**
     * Maximum number to retries retrieve metadata list for a given storage
     * queue ( in the errors occur in message stores)
     */
    private static final int MAX_META_DATA_RETRIEVAL_COUNT = 5;

    /**
     * In-memory message list scheduled to be delivered. These messages will be flushed
     * to subscriber.Used Map instead of Set because of https://wso2.org/jira/browse/MB-1624
     */
    private ConcurrentMap<Long, DeliverableAndesMetadata> readButUndeliveredMessages = new ConcurrentSkipListMap<>();

    /**
     * Map of slots read so far
     */
    private Map<String, Slot> slotsRead;

    /***
     * In case of a purge, we must store the timestamp when the purge was called.
     * This way we can identify messages received before that timestamp that fail and ignore them.
     */
    private long lastPurgedTimestamp;

    /**
     * Max number of messages to keep in buffer
     */
    private Integer maxNumberOfReadButUndeliveredMessages;

    /**
     * Used for asynchronously execute slot reassign task
     */
    private final ExecutorService executor;

    public MessageHandler(String queueName) {
        this.queueName = queueName;
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("AndesReleaseSlotTaskExecutor")
                .build();
        this.executor = Executors.newSingleThreadExecutor(namedThreadFactory);
        this.maxNumberOfReadButUndeliveredMessages = AndesConfigurationManager
                .readValue(AndesConfiguration.PERFORMANCE_TUNING_DELIVERY_MAX_READ_BUT_UNDELIVERED_MESSAGES);
        this.messageDeliveryManager = SlotDeliveryWorkerManager.getInstance();
        this.lastPurgedTimestamp = 0L;
        this.messageStore = AndesContext.getInstance().getMessageStore();
        this.slotsRead = new ConcurrentHashMap<>();
    }

    /**
     * Start delivering messages for queue
     *
     * @param queue queue to deliver messages
     * @throws AndesException
     */
    public void startMessageDelivery(StorageQueue queue) throws AndesException {
        messageDeliveryManager.startMessageDeliveryForQueue(queue);
    }

    /**
     * Stop delivering messages for queue
     *
     * @param queue queue to stop message delivery
     * @throws AndesException
     */
    public void stopMessageDelivery(StorageQueue queue) throws AndesException {
        messageDeliveryManager.stopDeliveryForQueue(queue);
    }

    /***
     * @return Last purged timestamp of queue.
     */
    public Long getLastPurgedTimestamp() {
        return lastPurgedTimestamp;
    }

    /**
     * Read messages from persistent store and buffer indicated
     * by the slot. This will filter messages for overlapped slots
     * as well.
     *
     * @param currentSlot slot of which messages to load
     * @return number of messages loaded to memory
     */
    public int bufferMessages(Slot currentSlot) throws AndesException {

        List<DeliverableAndesMetadata> messagesReadFromStore = readMessagesFromMessageStore(currentSlot);

        //if no messages are in the slot range, delete the slot from coordinator. No use of it
        if (messagesReadFromStore.isEmpty()) {
            SlotDeletionExecutor.getInstance().scheduleToDelete(currentSlot);
        }

        Slot trackedSlot = slotsRead.get(currentSlot.getId());
        if (trackedSlot == null) {
            slotsRead.put(currentSlot.getId(), currentSlot);
            trackedSlot = currentSlot;
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Overlapped slot received. Slot ID " + trackedSlot.getId());
            }
        }

        //filter and removed already buffered messages
        filterOverlappedMessages(trackedSlot, messagesReadFromStore);

        trackedSlot.incrementPendingMessageCount(messagesReadFromStore.size());

        for (DeliverableAndesMetadata message : messagesReadFromStore) {
            bufferMessage(message);
        }

        return messagesReadFromStore.size();
    }

    /**
     * Read messages from persistent store indicated by the message slot
     *
     * @param slot message slot to load messages
     * @return list of messages
     * @throws AndesException
     */
    private List<DeliverableAndesMetadata> readMessagesFromMessageStore(Slot slot) throws AndesException {
        List<DeliverableAndesMetadata> messagesRead;
        int numberOfRetries = 0;
        try {

            //Read messages in the slot
            messagesRead = messageStore.getMetadataList(slot, slot.getStorageQueueName(), slot.getStartMessageId(),
                    slot.getEndMessageId());

            if (log.isDebugEnabled()) {
                StringBuilder messageIDString = new StringBuilder();
                for (DeliverableAndesMetadata metadata : messagesRead) {
                    messageIDString.append(metadata.getMessageID()).append(" , ");
                }
                log.debug("Messages Read: " + messageIDString);
            }

        } catch (AndesException aex) {

            numberOfRetries = numberOfRetries + 1;

            if (numberOfRetries <= MAX_META_DATA_RETRIEVAL_COUNT) {

                String errorMsg = String.format(
                        "error occurred retrieving metadata" + " list for slot :" + " %s, retry count = %d",
                        slot.toString(), numberOfRetries);

                log.error(errorMsg, aex);
                messagesRead = messageStore.getMetadataList(slot, slot.getStorageQueueName(),
                        slot.getStartMessageId(), slot.getEndMessageId());
            } else {
                String errorMsg = String.format(
                        "error occurred retrieving metadata list for slot " + ": %s, in final attempt = %d. "
                                + "this slot will not be delivered " + "and become stale in message store",
                        slot.toString(), numberOfRetries);

                throw new AndesException(errorMsg, aex);
            }

        }

        if (log.isDebugEnabled()) {
            log.debug(
                    "Number of messages read from slot " + slot.getStartMessageId() + " - " + slot.getEndMessageId()
                            + " is " + messagesRead.size() + " storage queue= " + slot.getStorageQueueName());
        }

        return messagesRead;
    }

    /**
     * This will remove already buffered messages from the messagesRead list.
     * This is to avoid resending a message.
     *
     * @param slot     Slot which contains the given messages
     * @param messages Messages of the given slots
     */
    private void filterOverlappedMessages(Slot slot, List<DeliverableAndesMetadata> messages) {
        Iterator<DeliverableAndesMetadata> readMessageIterator = messages.iterator();
        while (readMessageIterator.hasNext()) {
            DeliverableAndesMetadata currentMessage = readMessageIterator.next();
            if (slot.checkIfMessageIsAlreadyAdded(currentMessage.getMessageID())) {
                if (log.isDebugEnabled()) {
                    log.debug("Tracker rejected message id= " + currentMessage.getMessageID() + " from buffering "
                            + "to deliver. This is an already buffered message");
                }
                readMessageIterator.remove();
            } else {
                currentMessage.changeSlot(slot);
                slot.addMessageToSlotIfAbsent(currentMessage);
            }
        }
    }

    /**
     * Get buffered messages
     *
     * @return Collection with DeliverableAndesMetadata
     */
    public Collection<DeliverableAndesMetadata> getReadButUndeliveredMessages() {
        return readButUndeliveredMessages.values();
    }

    /**
     * Buffer messages to be delivered
     *
     * @param message message metadata to buffer
     */
    public void bufferMessage(DeliverableAndesMetadata message) {
        readButUndeliveredMessages.putIfAbsent(message.getMessageID(), message);
        message.markAsBuffered();
        MessageTracer.trace(message, MessageTracer.METADATA_BUFFERED_FOR_DELIVERY);
    }

    /**
     * Remove buffered message given the id.
     *
     * @param messageId id of the message to be removed.
     */
    public void removeBufferedMessage(long messageId) {
        readButUndeliveredMessages.remove(messageId);
        if (log.isDebugEnabled()) {
            log.debug("Removing scheduled to send message from buffer with id: " + messageId);
        }
    }

    /**
     * Returns boolean variable saying whether this destination has room or not
     *
     * @return whether this destination has room or not
     */
    public boolean messageBufferHasRoom() {
        boolean hasRoom = true;
        if (readButUndeliveredMessages.size() >= maxNumberOfReadButUndeliveredMessages) {
            hasRoom = false;
        }
        return hasRoom;
    }

    /***
     * Clear the read-but-undelivered collection of messages of the given queue from memory
     *
     * @return Number of messages that was in the read-but-undelivered buffer
     */
    public int clearReadButUndeliveredMessages() {
        lastPurgedTimestamp = System.currentTimeMillis();
        int messageCount = readButUndeliveredMessages.size();
        readButUndeliveredMessages.clear();
        for (Slot slot : slotsRead.values()) {
            if (log.isDebugEnabled()) {
                log.debug("clear tracking of messages for slot = " + slot);
            }
            slot.markMessagesOfSlotAsReturned();
        }
        slotsRead.clear();
        return messageCount;
    }

    /**
     * Removes all the messages from read buffer, deletes the slots and deletes all messages from persistent storage.
     *
     * @return the number of messages that were deleted from the store
     */
    public int purgeMessagesOfQueue() throws AndesException {

        try {
            /*
             * Clear all slots assigned to the Queue. This should ideally stop
             * any messages being buffered during the purge. This call clears all slot associations
             * for the queue in all nodes calling to coordinator node via Thrift protocol (could take time).
             */
            MessagingEngine.getInstance().getSlotCoordinator().clearAllActiveSlotRelationsToQueue(queueName);

            //clear all in-memory messages
            purgeInMemoryMessagesOfQueue();

            // Delete messages from store
            int deletedMessageCount;
            if (!(DLCQueueUtils.isDeadLetterQueue(queueName))) {
                // delete all messages for the queue
                deletedMessageCount = messageStore.deleteAllMessageMetadata(queueName);
            } else {
                //delete all the messages in dlc
                deletedMessageCount = messageStore.clearDLCQueue(queueName);
            }
            return deletedMessageCount;

        } catch (AndesException e) {
            // This will be a store-specific error.
            throw new AndesException("Error occurred when purging queue from store : " + queueName, e);
        }

    }

    /**
     * Delete all in memory messages that are ready to be delivered.
     *
     * @return the number read but undelivered messages were removed from queue
     */
    public int purgeInMemoryMessagesOfQueue() {

        //clear all messages read to memory and return the slots
        return clearReadButUndeliveredMessages();

    }

    /**
     * Get message count for queue. This will query the persistent store.
     *
     * @return number of messages remaining in persistent store addressed to queue
     * @throws AndesException
     */
    public long getMessageCountForQueue() throws AndesException {
        long messageCount;
        if (!DLCQueueUtils.isDeadLetterQueue(queueName)) {
            messageCount = messageStore.getMessageCountForQueue(queueName);
        } else {
            messageCount = messageStore.getMessageCountForDLCQueue(queueName);
        }
        return messageCount;
    }

    /**
     * Delete message slot read
     *
     * @param slotToDelete slot to delete
     */
    public void deleteSlot(Slot slotToDelete) {
        if (log.isDebugEnabled()) {
            log.debug("Releasing tracking of messages for slot " + slotToDelete.toString());
        }
        slotToDelete.deleteAllMessagesInSlot();
        slotsRead.remove(slotToDelete.getId());
    }

    /**
     * Schedule to release all non empty slots read back to the coordinator
     */
    public void releaseAllSlots() {
        executor.submit(new SlotReAssignTask(queueName));
    }

    /**
     * Dump all message status of the slots read to file given
     *
     * @param fileToWrite      file to write
     * @param storageQueueName name of storage queue
     * @throws AndesException
     */
    public void dumpAllSlotInformationToFile(File fileToWrite, String storageQueueName) throws AndesException {

        FileWriter information = null;
        try {
            information = new FileWriter(fileToWrite, true);
            for (Slot slot : slotsRead.values()) {
                List<DeliverableAndesMetadata> messagesOfSlot = slot.getAllMessagesOfSlot();
                if (!messagesOfSlot.isEmpty()) {

                    int writerFlushCounter = 0;
                    for (DeliverableAndesMetadata message : messagesOfSlot) {
                        information.append(storageQueueName).append(",").append(slot.getId()).append(",")
                                .append(message.dumpMessageStatus()).append("\n");
                        writerFlushCounter = writerFlushCounter + 1;
                        if (writerFlushCounter % 10 == 0) {
                            information.flush();
                        }
                    }

                    information.flush();
                }
            }
            information.flush();

        } catch (FileNotFoundException e) {
            log.error("File to write is not found", e);
            throw new AndesException("File to write is not found", e);
        } catch (IOException e) {
            log.error("Error while dumping message status to file", e);
            throw new AndesException("Error while dumping message status to file", e);
        } finally {
            try {
                if (information != null) {
                    information.close();
                }
            } catch (IOException e) {
                log.error("Error while closing file when dumping message status to file", e);
            }
        }
    }

}