net.timewalker.ffmq4.local.destination.LocalQueue.java Source code

Java tutorial

Introduction

Here is the source code for net.timewalker.ffmq4.local.destination.LocalQueue.java

Source

/*
 * This file is part of FFMQ.
 *
 * FFMQ is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * FFMQ is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with FFMQ; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package net.timewalker.ffmq4.local.destination;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import javax.jms.DeliveryMode;
import javax.jms.JMSException;
import javax.jms.Queue;

import net.timewalker.ffmq4.FFMQException;
import net.timewalker.ffmq4.common.message.AbstractMessage;
import net.timewalker.ffmq4.common.message.MessageSelector;
import net.timewalker.ffmq4.local.FFMQEngine;
import net.timewalker.ffmq4.local.MessageLock;
import net.timewalker.ffmq4.local.MessageLockSet;
import net.timewalker.ffmq4.local.TransactionItem;
import net.timewalker.ffmq4.local.TransactionSet;
import net.timewalker.ffmq4.local.session.LocalMessageConsumer;
import net.timewalker.ffmq4.local.session.LocalQueueBrowserCursor;
import net.timewalker.ffmq4.local.session.LocalSession;
import net.timewalker.ffmq4.management.destination.definition.QueueDefinition;
import net.timewalker.ffmq4.storage.data.DataStoreFullException;
import net.timewalker.ffmq4.storage.message.MessageSerializationLevel;
import net.timewalker.ffmq4.storage.message.MessageStore;
import net.timewalker.ffmq4.storage.message.impl.BlockFileMessageStore;
import net.timewalker.ffmq4.storage.message.impl.InMemoryMessageStore;
import net.timewalker.ffmq4.utils.ErrorTools;
import net.timewalker.ffmq4.utils.async.AsyncTask;
import net.timewalker.ffmq4.utils.concurrent.BlockingBoundedFIFO;
import net.timewalker.ffmq4.utils.concurrent.CopyOnWriteList;
import net.timewalker.ffmq4.utils.concurrent.SynchronizationBarrier;
import net.timewalker.ffmq4.utils.concurrent.WaitTimeoutException;
import net.timewalker.ffmq4.utils.watchdog.ActiveObject;
import net.timewalker.ffmq4.utils.watchdog.ActivityWatchdog;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <p>Implementation for a local JMS {@link Queue}</p>
 */
public final class LocalQueue extends AbstractLocalDestination implements Queue, LocalQueueMBean, ActiveObject {
    private static final Log log = LogFactory.getLog(LocalQueue.class);

    // Scheduler thread handling delayed redeliveries
    private static final Timer redeliveryTimer = new Timer(true);

    // Definition
    private FFMQEngine engine;
    private QueueDefinition queueDef;

    // Message stores
    private MessageStore volatileStore;
    private MessageStore persistentStore;
    private Object storeLock = new Object();

    // Statistics
    private AtomicLong sentToQueueCount = new AtomicLong();
    private AtomicLong receivedFromQueueCount = new AtomicLong();
    private AtomicLong acknowledgedGetCount = new AtomicLong();
    private AtomicLong rollbackedGetCount = new AtomicLong();
    private AtomicLong expiredCount = new AtomicLong();

    // Settings
    private long inactivityTimeout;
    private long redeliveryDelay;
    private boolean traceEnabled = log.isTraceEnabled();

    // Runtime
    private boolean pendingChanges;
    private long lastActivity;
    private volatile int consumerOffset = 0; // Used for a round-robin-like consumer wake-up
    private BlockingBoundedFIFO<AbstractMessage> notificationQueue;

    /**
     * Constructor
     */
    public LocalQueue(FFMQEngine engine, QueueDefinition queueDef) throws JMSException {
        super(queueDef);
        this.engine = engine;
        this.queueDef = queueDef;

        // Create notification FIFO
        int notificationQueueMaxSize = Math.max(
                engine.getSetup().getNotificationAsyncTaskManagerThreadPoolMaxSize() + 1,
                engine.getSetup().getInternalNotificationQueueMaxSize());
        this.notificationQueue = new BlockingBoundedFIFO<>(notificationQueueMaxSize, 5 * 1000); /* 5s timeout */

        // Init volatile store
        if (queueDef.getMaxNonPersistentMessages() > 0) {
            this.volatileStore = new InMemoryMessageStore(queueDef);
            this.volatileStore.init();
        }

        // Init persistent store
        if (queueDef.hasPersistentStore()) {
            this.persistentStore = new BlockFileMessageStore(queueDef, engine.getDiskIOAsyncTaskManager());
            this.persistentStore.init();
        }

        this.inactivityTimeout = engine.getSetup().getWatchdogConsumerInactivityTimeout() * 1000L;
        this.redeliveryDelay = engine.getSetup().getRedeliveryDelay();
        this.lastActivity = System.currentTimeMillis();
        ActivityWatchdog.getInstance().register(this);
    }

    /**
     * Get the queue definition
     */
    public QueueDefinition getDefinition() {
        return queueDef;
    }

    /*
     * (non-Javadoc)
     * @see javax.jms.Queue#getQueueName()
     */
    @Override
    public String getQueueName() {
        return getName();
    }

    /* (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.AbstractLocalDestination#putLocked(net.timewalker.ffmq4.common.message.AbstractMessage, net.timewalker.ffmq4.local.session.LocalSession, net.timewalker.ffmq4.local.MessageLockSet)
     */
    @Override
    public boolean putLocked(AbstractMessage message, LocalSession session, MessageLockSet locks)
            throws JMSException {
        checkNotClosed();
        checkTransactionLock();

        // Consistency check
        if (!message.isInternalCopy())
            throw new FFMQException("Message instance is not an FFMQ internal copy !", "CONSISTENCY_ERROR");

        // Dispatch message to the adequate store
        MessageStore targetStore;
        if (message.getJMSDeliveryMode() == DeliveryMode.NON_PERSISTENT) {
            // Use volatile store if possible, otherwise fallback to persistent store
            targetStore = volatileStore != null ? volatileStore : persistentStore;
        } else
            targetStore = persistentStore;

        if (targetStore == null)
            throw new FFMQException("Queue does not support this delivery mode : "
                    + (message.getJMSDeliveryMode() == DeliveryMode.NON_PERSISTENT ? "DeliveryMode.NON_PERSISTENT"
                            : "DeliveryMode.PERSISTENT"),
                    "INVALID_DELIVERY_MODE");

        int newHandle;
        synchronized (storeLock) {
            newHandle = targetStore.store(message);
            if (newHandle == -1) {
                // No space left for this message in the target store
                if (targetStore == volatileStore && persistentStore != null && queueDef.isOverflowToPersistent()) {
                    // Fallback to persistent store if possible
                    targetStore = persistentStore;
                    newHandle = targetStore.store(message);
                }

                // Cannot store the message anywhere
                if (newHandle == -1)
                    throw new DataStoreFullException("Cannot store message : queue is full : " + getName());
            }

            targetStore.lock(newHandle);
            locks.add(newHandle, targetStore.getDeliveryMode(), this, message);
        }

        if (message.getJMSDeliveryMode() == DeliveryMode.PERSISTENT && requiresTransactionalUpdate()) {
            pendingChanges = true;
            return true;
        } else
            return false;
    }

    /**
     * Unlock a message.
     * Listeners are automatically notified of the new message availability.
     */
    public void unlockAndDeliver(MessageLock lockRef) throws JMSException {
        MessageStore targetStore;
        if (lockRef.getDeliveryMode() == DeliveryMode.NON_PERSISTENT)
            targetStore = volatileStore;
        else
            targetStore = persistentStore;

        int handle = lockRef.getHandle();
        AbstractMessage message = lockRef.getMessage();
        synchronized (storeLock) {
            targetStore.unlock(handle);
        }
        sentToQueueCount.incrementAndGet();

        sendAvailabilityNotification(message);
    }

    /**
     * Remove a locked message from this queue. The message is deleted from the underlying store.
     */
    public void removeLocked(MessageLock lockRef) throws JMSException {
        checkTransactionLock();

        MessageStore targetStore;
        if (lockRef.getDeliveryMode() == DeliveryMode.NON_PERSISTENT)
            targetStore = volatileStore;
        else {
            targetStore = persistentStore;
            if (requiresTransactionalUpdate())
                pendingChanges = true;
        }

        synchronized (storeLock) {
            targetStore.delete(lockRef.getHandle());
        }
    }

    /**
     * Commit get operations on this queue (messages are removed)
     * @return true if a store commit is required to ensure data safety
     */
    public boolean remove(LocalSession localSession, TransactionItem[] items) throws JMSException {
        checkNotClosed();
        checkTransactionLock();

        int volatileCommitted = 0;
        int persistentCommitted = 0;

        synchronized (storeLock) {
            for (int n = 0; n < items.length; n++) {
                TransactionItem transactionItem = items[n];
                if (transactionItem.getDestination() != this)
                    continue;

                if (traceEnabled)
                    log.trace(localSession + " COMMIT " + transactionItem.getMessageId());

                // Delete message from store
                if (transactionItem.getDeliveryMode() == DeliveryMode.PERSISTENT) {
                    persistentStore.delete(transactionItem.getHandle());
                    persistentCommitted++;
                } else {
                    volatileStore.delete(transactionItem.getHandle());
                    volatileCommitted++;
                }
            }
        }
        acknowledgedGetCount.addAndGet(volatileCommitted + persistentCommitted);

        if (persistentCommitted > 0 && requiresTransactionalUpdate()) {
            pendingChanges = true;
            return true;
        } else
            return false;
    }

    /**
     * Rollback get operations on this queue (messages are unlocked)..
     * Consumers are notified of rollbacked messages availability
     * @return true if a commit is required to ensure data safety
     */
    public boolean redeliverLocked(TransactionItem[] items, MessageLockSet locks) throws JMSException {
        checkNotClosed();
        checkTransactionLock();

        int volatileRollbacked = 0;
        int persistentRollbacked = 0;

        synchronized (storeLock) {
            for (int n = 0; n < items.length; n++) {
                TransactionItem transactionItem = items[n];
                if (transactionItem.getDestination() != this)
                    continue; // Not for us

                MessageStore store = transactionItem.getDeliveryMode() == DeliveryMode.PERSISTENT ? persistentStore
                        : volatileStore;
                int handle = transactionItem.getHandle();

                // Retrieve message content
                AbstractMessage msg = store.retrieve(handle);

                // Update redelivered flag both in memory and message store
                msg.setJMSRedelivered(true);
                handle = store.replace(handle, msg);

                if (redeliveryDelay > 0) {
                    // Keep the message locked so it cannot be re-consumed immediately                   
                    // and schedule message unlock after redeliveryDelay milliseconds
                    redeliveryTimer.schedule(new RedeliveryTask(msg, store, handle), redeliveryDelay);
                } else {
                    // Store lock for later release
                    locks.add(handle, store.getDeliveryMode(), this, msg);
                }

                if (transactionItem.getDeliveryMode() == DeliveryMode.PERSISTENT)
                    persistentRollbacked++;
                else
                    volatileRollbacked++;
            }
        }
        rollbackedGetCount.addAndGet(volatileRollbacked + persistentRollbacked);

        if (persistentRollbacked > 0 && requiresTransactionalUpdate()) {
            pendingChanges = true;
            return true;
        } else
            return false;
    }

    /*
     * (non-Javadoc)
     * @see net.timewalker.ffmq4.utils.Committable#commitChanges(net.timewalker.ffmq4.utils.concurrent.SynchronizationBarrier)
     */
    @Override
    public void commitChanges(SynchronizationBarrier barrier) throws JMSException {
        checkNotClosed();
        checkTransactionLock();

        if (persistentStore != null) {
            long start = System.currentTimeMillis();
            synchronized (storeLock) {
                persistentStore.commitChanges(barrier);
            }
            long end = System.currentTimeMillis();
            notifyCommitTime(end - start);

            // Clear pending commit flag
            pendingChanges = false;
        }
    }

    /** See RedeliveryTask */
    protected void redeliverMessage(AbstractMessage msg, MessageStore store, int handle) {
        try {
            synchronized (storeLock) {
                // Unlock message in store
                store.unlock(handle);
                if (traceEnabled)
                    log.trace("(Deferred) UNLOCKED " + msg.getJMSMessageID());
            }

            // Dispatch notification
            sendAvailabilityNotification(msg);
        } catch (JMSException e) {
            ErrorTools.log(e, log);
        }
    }

    /**
     * Get the first available message from this destination (matching an optional message selector).
     * If a message is found, the transaction set is updated accordingly.
     * @return a message or null if there is no available message (or queue is closed)
     */
    public AbstractMessage get(LocalSession localSession, TransactionSet transactionSet, MessageSelector selector)
            throws JMSException {
        if (closed)
            return null;

        this.lastActivity = System.currentTimeMillis();

        AbstractMessage msg = null;

        // Search in volatile store first
        if (volatileStore != null) {
            msg = getFromStore(localSession, volatileStore, transactionSet, selector);

            // Then in persistent store
            if (msg == null && persistentStore != null)
                msg = getFromStore(localSession, persistentStore, transactionSet, selector);
        } else if (persistentStore != null)
            msg = getFromStore(localSession, persistentStore, transactionSet, selector);

        return msg;
    }

    /**
     * Browse a message in this queue 
     * @param cursor browser cursor
     * @param selector a message selector or null
     * @return a matching message or null
     * @throws JMSException on internal error
     */
    public AbstractMessage browse(LocalQueueBrowserCursor cursor, MessageSelector selector) throws JMSException {
        // Reset cursor to last known position
        cursor.reset();

        // Search in volatile store first
        if (volatileStore != null) {
            AbstractMessage msg = browseStore(volatileStore, cursor, selector);
            if (msg != null) {
                cursor.move();
                return msg;
            }
        }

        // Then in persistent store
        if (persistentStore != null) {
            AbstractMessage msg = browseStore(persistentStore, cursor, selector);
            if (msg != null) {
                cursor.move();
                return msg;
            }
        }

        // Nothing found, set EOQ flag on cursor 
        cursor.setEndOfQueueReached();

        return null;
    }

    private AbstractMessage browseStore(MessageStore store, LocalQueueBrowserCursor cursor,
            MessageSelector selector) throws JMSException {
        AbstractMessage result = null;
        List<Integer> expiredHandles = null;

        long now = System.currentTimeMillis();
        synchronized (storeLock) {
            int current = store.first();

            // Skip to initial position
            while (current != -1 && cursor.position() > cursor.skipped()) {
                cursor.skip();
                current = store.next(current);
            }

            while (current != -1) {
                // Skip locked messages
                if (!store.isLocked(current)) {
                    // Retrieve the message
                    AbstractMessage msg = store.retrieve(current);

                    // Check expiration
                    if (msg.getJMSExpiration() > 0 && msg.getJMSExpiration() < now) {
                        if (expiredHandles == null)
                            expiredHandles = new ArrayList<>();
                        store.lock(current);
                        expiredHandles.add(Integer.valueOf(current));
                        current = store.next(current);
                        continue;
                    }

                    // Check selector
                    if (selector == null) {
                        result = msg;
                        break;
                    } else {
                        msg.ensureDeserializationLevel(MessageSerializationLevel.ALL_HEADERS);
                        if (selector.matches(msg)) {
                            result = msg;
                            break;
                        }
                    }
                }

                cursor.skip();
                current = store.next(current);
            }
        }

        // Take care of expired messages
        if (expiredHandles != null) {
            openTransaction();
            try {
                for (int i = 0; i < expiredHandles.size(); i++) {
                    int expiredHandle = expiredHandles.get(i).intValue();
                    synchronized (storeLock) {
                        store.delete(expiredHandle);
                    }
                    expiredCount.incrementAndGet();
                }
                commitChanges(null); // Async commit
            } finally {
                closeTransaction();
            }
        }

        return result;
    }

    private AbstractMessage getFromStore(LocalSession localSession, MessageStore store,
            TransactionSet transactionSet, MessageSelector selector) throws JMSException {
        AbstractMessage result = null;
        List<Integer> expiredHandles = null;

        synchronized (storeLock) {
            int current = store.first();
            while (current != -1) {
                // Skip locked messages
                if (!store.isLocked(current)) {
                    // Retrieve the message
                    AbstractMessage msg = store.retrieve(current);

                    // Check expiration
                    if (msg.getJMSExpiration() > 0 && msg.getJMSExpiration() < lastActivity) {
                        if (expiredHandles == null)
                            expiredHandles = new ArrayList<>();
                        store.lock(current);
                        expiredHandles.add(Integer.valueOf(current));
                        current = store.next(current);
                        continue;
                    }

                    // Check selector
                    boolean matchesSelector;
                    if (selector != null) {
                        msg.ensureDeserializationLevel(MessageSerializationLevel.ALL_HEADERS);
                        matchesSelector = selector.matches(msg);
                    } else
                        matchesSelector = true;

                    if (matchesSelector) {
                        store.lock(current);

                        if (traceEnabled)
                            log.trace(localSession + " LOCKED " + msg.getJMSMessageID());

                        transactionSet.add(current, msg.getJMSMessageID(), store.getDeliveryMode(), this);
                        receivedFromQueueCount.incrementAndGet();
                        result = msg;
                        break;
                    }
                }

                current = store.next(current);
            }
        }

        // Take care of expired messages
        if (expiredHandles != null) {
            openTransaction();
            try {
                for (int i = 0; i < expiredHandles.size(); i++) {
                    int expiredHandle = expiredHandles.get(i).intValue();
                    synchronized (storeLock) {
                        store.delete(expiredHandle);
                    }
                    expiredCount.incrementAndGet();
                }
                commitChanges(null); // Async commit
            } finally {
                closeTransaction();
            }
        }

        return result;
    }

    /**
     * Purge some messages from the buffer
     */
    public void purge(MessageSelector selector) throws JMSException {
        if (volatileStore != null)
            purgeStore(volatileStore, selector);
        if (persistentStore != null) {
            openTransaction();
            try {
                purgeStore(persistentStore, selector);
                commitChanges();
            } finally {
                closeTransaction();
            }
        }
    }

    private void purgeStore(MessageStore store, MessageSelector selector) throws JMSException {
        synchronized (storeLock) {
            int current = store.first();
            while (current != -1) {
                int next = store.next(current);

                // Skip locked messages
                if (!store.isLocked(current)) {
                    if (selector != null) {
                        AbstractMessage msg = store.retrieve(current);
                        msg.ensureDeserializationLevel(MessageSerializationLevel.ALL_HEADERS);
                        if (selector.matches(msg))
                            store.delete(current);
                    } else
                        store.delete(current);
                }

                current = next;
            }
        }
    }

    /**
     * Notify a consumer that a message is probably available for it to retrieve
     */
    private void notifyConsumer(AbstractMessage message) {
        LocalMessageConsumer singleConsumer = null;
        CopyOnWriteList<LocalMessageConsumer> consumersSnapshot = null;
        synchronized (localConsumers) {
            switch (localConsumers.size()) {
            case 0:
                return; // Nobody's listening
            case 1:
                singleConsumer = localConsumers.get(0);
                break; // Single consumer
            default: // Multiple consumers
                consumersSnapshot = localConsumers.fastCopy();
            }
        }

        if (singleConsumer != null)
            notifySingleConsumer(singleConsumer, message);
        else
            notifyNextConsumer(consumersSnapshot, message);
    }

    private void notifySingleConsumer(LocalMessageConsumer consumer, AbstractMessage message) {
        try {
            // Check message selector
            if (message != null) {
                MessageSelector consumerSelector = consumer.getReceiveSelector();
                if (consumerSelector != null) {
                    message.ensureDeserializationLevel(MessageSerializationLevel.ALL_HEADERS);
                    if (!consumerSelector.matches(message))
                        return;
                }
            }

            consumer.wakeUp();
        } catch (JMSException e) {
            ErrorTools.log(e, log);
        }
    }

    private void notifyNextConsumer(CopyOnWriteList<LocalMessageConsumer> consumersSnapshot,
            AbstractMessage message) {
        // Find a consumer to notify
        int localConsumersCount = consumersSnapshot.size();
        int currentOffset = consumerOffset++; // Copy current offset (value is volatile and should not change during the following loop)
        for (int n = 0; n < localConsumersCount; n++) {
            int offset = ((n + currentOffset) % localConsumersCount);
            LocalMessageConsumer consumer = consumersSnapshot.get(offset);

            // Check that the consumer connection is started
            if (!consumer.getSession().getConnection().isStarted())
                continue;

            // Check message selector
            if (message != null) {
                MessageSelector consumerSelector = consumer.getReceiveSelector();
                if (consumerSelector != null) {
                    message.ensureDeserializationLevel(MessageSerializationLevel.ALL_HEADERS);

                    try {
                        if (!consumerSelector.matches(message))
                            continue;
                    } catch (JMSException e) {
                        ErrorTools.log(e, log);
                        continue;
                    }
                }
            }

            try {
                consumer.wakeUp();
                break;
            } catch (JMSException e) {
                ErrorTools.log(e, log);
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.LocalDestinationMBean#getSize()
     */
    @Override
    public int getSize() {
        int size = 0;
        synchronized (storeLock) {
            if (volatileStore != null)
                size += volatileStore.size();
            if (persistentStore != null)
                size += persistentStore.size();
        }
        return size;
    }

    /* (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.LocalQueueMBean#getMemoryStoreUsage()
     */
    @Override
    public int getMemoryStoreUsage() {
        return volatileStore != null ? volatileStore.getAbsoluteStoreUsage() : -1;
    }

    /* (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.LocalQueueMBean#getPersistentStoreUsage()
     */
    @Override
    public int getPersistentStoreUsage() {
        return persistentStore != null ? persistentStore.getAbsoluteStoreUsage() : -1;
    }

    /*
     * (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.LocalDestinationMBean#resetStats()
     */
    @Override
    public void resetStats() {
        super.resetStats();
        sentToQueueCount.set(0);
        receivedFromQueueCount.set(0);
        acknowledgedGetCount.set(0);
        rollbackedGetCount.set(0);
        expiredCount.set(0);
    }

    /*
     *  (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();

        sb.append("Queue{");
        sb.append(getName());
        sb.append("}[size=");
        sb.append(getSize());
        sb.append(",consumers=");
        sb.append(localConsumers.size());
        sb.append(",in=");
        sb.append(sentToQueueCount);
        sb.append(",out=");
        sb.append(receivedFromQueueCount);
        sb.append(",ack=");
        sb.append(acknowledgedGetCount);
        sb.append(",rollback=");
        sb.append(rollbackedGetCount);
        sb.append(",expired=");
        sb.append(expiredCount);
        sb.append("]");

        return sb.toString();
    }

    /*
     * (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.AbstractLocalDestination#close()
     */
    @Override
    public final void close() throws JMSException {
        synchronized (closeLock) {
            if (closed)
                return;
            closed = true;
        }

        ActivityWatchdog.getInstance().unregister(this);

        synchronized (storeLock) {
            if (volatileStore != null) {
                volatileStore.close();

                // Delete message store if the queue was temporary
                if (queueDef.isTemporary())
                    volatileStore.delete();
            }
            if (persistentStore != null) {
                persistentStore.close();

                // Delete message store if the queue was temporary
                if (queueDef.isTemporary())
                    persistentStore.delete();
            }
        }

        if (!localConsumers.isEmpty()) {
            CopyOnWriteList<LocalMessageConsumer> consumers = localConsumers.fastCopy();
            for (int n = 0; n < consumers.size(); n++) {
                LocalMessageConsumer consumer = consumers.get(n);
                try {
                    consumer.close();
                } catch (JMSException e) {
                    ErrorTools.log(e, log);
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.LocalQueueMBean#getSentToQueueCount()
     */
    @Override
    public long getSentToQueueCount() {
        return sentToQueueCount.get();
    }

    /*
     * (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.LocalQueueMBean#getReceivedFromQueueCount()
     */
    @Override
    public long getReceivedFromQueueCount() {
        return receivedFromQueueCount.get();
    }

    /*
     * (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.LocalQueueMBean#getAcknowledgedGetCount()
     */
    @Override
    public long getAcknowledgedGetCount() {
        return acknowledgedGetCount.get();
    }

    /*
     * (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.LocalQueueMBean#getRollbackedGetCount()
     */
    @Override
    public long getRollbackedGetCount() {
        return rollbackedGetCount.get();
    }

    /*
     * (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.LocalQueueMBean#getExpiredCount()
     */
    @Override
    public long getExpiredCount() {
        return expiredCount.get();
    }

    /* (non-Javadoc)
     * @see net.timewalker.ffmq4.utils.watchdog.ActiveObject#getLastActivity()
     */
    @Override
    public long getLastActivity() {
        return lastActivity;
    }

    /* (non-Javadoc)
     * @see net.timewalker.ffmq4.utils.watchdog.ActiveObject#getTimeoutDelay()
     */
    @Override
    public long getTimeoutDelay() {
        return inactivityTimeout;
    }

    /* (non-Javadoc)
     * @see net.timewalker.ffmq4.utils.watchdog.ActiveObject#onActivityTimeout()
     */
    @Override
    public boolean onActivityTimeout() throws Exception {
        // Called by watchdog if queue is inactive
        if (closed)
            return true;

        if (getSize() == 0)
            return false; // Queue is empty

        // Notify next consumer
        notifyConsumer(null);

        return false;
    }

    protected void processAvailabilityNotificationQueue() {
        while (!closed) {
            AbstractMessage message = notificationQueue.removeFirst();
            if (message == null)
                return;

            notifyConsumer(message);
        }
    }

    private void sendAvailabilityNotification(AbstractMessage message) throws JMSException {
        if (localConsumers.isEmpty())
            return;

        try {
            // May block if FIFO is full
            notificationQueue.addLast(message);
        } catch (WaitTimeoutException e) {
            log.error("Cannot enqueue notification " + e);
            return;
        }

        engine.getNotificationAsyncTaskManager().execute(notificationTask);
    }

    //-------------------------------------------------------------------------------

    private final class RedeliveryTask extends TimerTask {
        // Attributes
        private AbstractMessage msg;
        private MessageStore store;
        private int handle;

        /**
        * Constructor
        */
        public RedeliveryTask(AbstractMessage msg, MessageStore store, int handle) {
            this.msg = msg;
            this.store = store;
            this.handle = handle;
        }

        /* (non-Javadoc)
         * @see java.util.TimerTask#run()
         */
        @Override
        public void run() {
            redeliverMessage(msg, store, handle);
        }
    }

    /* (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.AbstractLocalDestination#hasPendingChanges()
     */
    @Override
    protected boolean hasPendingChanges() {
        return pendingChanges;
    }

    /* (non-Javadoc)
     * @see net.timewalker.ffmq4.local.destination.AbstractLocalDestination#hasTransactionSupport()
     */
    @Override
    protected boolean requiresTransactionalUpdate() {
        return persistentStore != null && persistentStore.isFailSafe();
    }

    //-------------------------------------------------------------------------------

    private final NotificationTask notificationTask = new NotificationTask();

    private final class NotificationTask implements AsyncTask {
        /**
        * Constructor
        */
        public NotificationTask() {
            super();
        }

        /* (non-Javadoc)
         * @see net.timewalker.ffmq4.utils.async.AsyncTask#isMergeable()
         */
        @Override
        public boolean isMergeable() {
            return true;
        }

        /* (non-Javadoc)
         * @see net.timewalker.ffmq4.utils.async.AsyncTask#execute()
         */
        @Override
        public void execute() {
            processAvailabilityNotificationQueue();
        }
    }
}