/*
* 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.ffmq3.local.destination;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import javax.jms.DeliveryMode;
import javax.jms.JMSException;
import javax.jms.Queue;
import net.timewalker.ffmq3.FFMQException;
import net.timewalker.ffmq3.common.message.AbstractMessage;
import net.timewalker.ffmq3.common.message.MessageSelector;
import net.timewalker.ffmq3.local.FFMQEngine;
import net.timewalker.ffmq3.local.MessageLock;
import net.timewalker.ffmq3.local.MessageLockSet;
import net.timewalker.ffmq3.local.TransactionItem;
import net.timewalker.ffmq3.local.TransactionSet;
import net.timewalker.ffmq3.local.session.LocalMessageConsumer;
import net.timewalker.ffmq3.local.session.LocalQueueBrowserCursor;
import net.timewalker.ffmq3.local.session.LocalSession;
import net.timewalker.ffmq3.management.destination.definition.QueueDefinition;
import net.timewalker.ffmq3.storage.data.DataStoreFullException;
import net.timewalker.ffmq3.storage.message.MessageSerializationLevel;
import net.timewalker.ffmq3.storage.message.MessageStore;
import net.timewalker.ffmq3.storage.message.impl.BlockFileMessageStore;
import net.timewalker.ffmq3.storage.message.impl.InMemoryMessageStore;
import net.timewalker.ffmq3.utils.Committable;
import net.timewalker.ffmq3.utils.ErrorTools;
import net.timewalker.ffmq3.utils.concurrent.CopyOnWriteList;
import net.timewalker.ffmq3.utils.concurrent.SynchronizationBarrier;
import net.timewalker.ffmq3.utils.watchdog.ActiveObject;
import net.timewalker.ffmq3.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, Committable
{
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 store
private MessageStore volatileStore;
private MessageStore persistentStore;
private Object storeLock = new Object();
// Statistics
private volatile long sentToQueueCount = 0;
private volatile long receivedFromQueueCount = 0;
private volatile long acknowledgedGetCount = 0;
private volatile long rollbackedGetCount = 0;
// Settings
private long inactivityTimeout;
private long redeliveryDelay;
private boolean traceEnabled = log.isTraceEnabled();
// Runtime
private long lastActivity;
private volatile int consumerOffset = 0; // Used for a round-robin-like consumer wake-up
/**
* Constructor
*/
public LocalQueue( FFMQEngine engine , QueueDefinition queueDef ) throws JMSException
{
super(queueDef);
this.engine = engine;
this.queueDef = queueDef;
// Init volatile store
if (queueDef.getMaxNonPersistentMessages() > 0)
{
this.volatileStore = new InMemoryMessageStore(queueDef);
this.volatileStore.init();
}
// Init persistent store
if (queueDef.hasDataFiles())
{
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()
*/
public String getQueueName()
{
return getName();
}
/**
* Put a new message in the queue.
* Listeners are automatically notified of the new message availability.
* @return true if a commit is required to ensure data safety
*/
public boolean put( AbstractMessage message ) throws JMSException
{
return put(message,null);
}
/**
* Put a new message in the queue. The message is locked and the lock registered in the provided handle set
* @return true if a commit is required to ensure data safety
*/
public boolean putLocked( AbstractMessage message , MessageLockSet locks ) throws JMSException
{
return put(message,locks);
}
private boolean put( AbstractMessage message , MessageLockSet locks ) throws JMSException
{
checkNotClosed();
// 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)
{
// 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());
}
if (locks != null)
{
targetStore.lock(newHandle);
locks.add(newHandle, targetStore.getDeliveryMode(), this);
}
}
sentToQueueCount++;
// Wake up waiting consumers
if (locks == null)
messageAvailable(message);
return (message.getJMSDeliveryMode() == DeliveryMode.PERSISTENT);
}
/**
* 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;
AbstractMessage message;
synchronized (storeLock)
{
int handle = lockRef.getHandle();
message = targetStore.retrieve(handle);
targetStore.unlock(handle);
}
messageAvailable(message);
}
/**
* Remove a locked message from the queue
*/
public void removeLocked( MessageLock lockRef ) throws JMSException
{
MessageStore targetStore;
if (lockRef.getDeliveryMode() == DeliveryMode.NON_PERSISTENT)
targetStore = volatileStore;
else
targetStore = persistentStore;
synchronized (storeLock)
{
targetStore.delete(lockRef.getHandle());
}
}
/**
* Commit get operations on this queue (messages are removed)
* @return true if a commit is required to ensure data safety
*/
public boolean remove( LocalSession localSession , TransactionItem[] items ) throws JMSException
{
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 += volatileCommitted + persistentCommitted;
}
return persistentCommitted > 0;
}
/**
* 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 redeliver( LocalSession localSession , TransactionItem[] items ) throws JMSException
{
int volatileRollbacked = 0;
int persistentRollbacked = 0;
List newEntries = null;
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 the message unlock afetr redeliveryDelay milliseconds
redeliveryTimer.schedule(new RedeliveryTask(msg,store,handle),
redeliveryDelay);
}
else
{
// Unlock message in store
store.unlock(handle);
if (traceEnabled)
log.trace(localSession+" UNLOCKED "+transactionItem.getMessageId());
// Add to dispatch list
if (newEntries == null)
newEntries = new ArrayList();
newEntries.add(msg);
}
if (transactionItem.getDeliveryMode() == DeliveryMode.PERSISTENT)
persistentRollbacked++;
else
volatileRollbacked++;
}
rollbackedGetCount += volatileRollbacked + persistentRollbacked;
}
// Dispatch notifications
if (newEntries != null)
for (int n = 0 ; n < newEntries.size() ; n++)
messageAvailable((AbstractMessage)newEntries.get(n));
return persistentRollbacked > 0;
}
/*
* (non-Javadoc)
* @see net.timewalker.ffmq3.utils.Commitable#commitChanges(net.timewalker.ffmq3.utils.concurrent.SynchronizationBarrier)
*/
public void commitChanges( SynchronizationBarrier barrier ) throws JMSException
{
if (persistentStore != null)
{
synchronized (storeLock)
{
persistentStore.commitChanges(barrier);
}
}
}
/** 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
messageAvailable(msg);
}
catch (JMSException e)
{
ErrorTools.log(e, log);
}
}
/**
* Get a message from this destination
*/
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
{
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)
{
int handleToDelete = current;
current = store.next(current);
store.delete(handleToDelete);
continue;
}
// Check selector
if (selector == null)
{
return msg;
}
else
{
msg.ensureDeserializationLevel(MessageSerializationLevel.ALL_HEADERS);
if (selector.matches(msg))
return msg;
}
}
cursor.skip();
current = store.next(current);
}
return null;
}
}
private AbstractMessage getFromStore( LocalSession localSession ,
MessageStore store ,
TransactionSet transactionSet ,
MessageSelector selector ) throws JMSException
{
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)
{
int handleToDelete = current;
current = store.next(current);
store.delete(handleToDelete);
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++;
return msg;
}
}
current = store.next(current);
}
return null;
}
}
/**
* Purge some messages from the buffer
*/
public void purge( MessageSelector selector ) throws JMSException
{
if (volatileStore != null)
purgeStore(volatileStore,selector);
if (persistentStore != null)
purgeStore(persistentStore,selector);
}
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;
}
}
}
/**
* Called when a message is available
*/
protected void messageAvailable( AbstractMessage message )
{
LocalMessageConsumer singleConsumer = null;
CopyOnWriteList consumersSnapshot = null;
synchronized (localConsumers)
{
switch (localConsumers.size())
{
case 0 : return; // Nobody's listening
case 1 : singleConsumer = (LocalMessageConsumer)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;
}
}
if (consumer.mayBlock())
engine.getDeliveryAsyncTaskManager().execute(consumer);
else
consumer.messageAvailable();
}
catch (JMSException e)
{
ErrorTools.log(e, log);
consumer.getSession().getConnection().exceptionOccured(e);
}
}
private void notifyNextConsumer( CopyOnWriteList 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 = (LocalMessageConsumer)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);
consumer.getSession().getConnection().exceptionOccured(e);
break;
}
}
}
try
{
if (consumer.mayBlock())
engine.getDeliveryAsyncTaskManager().execute(consumer);
else
consumer.messageAvailable();
break;
}
catch (JMSException e)
{
ErrorTools.log(e, log);
consumer.getSession().getConnection().exceptionOccured(e);
}
}
}
/*
* (non-Javadoc)
* @see net.timewalker.ffmq3.local.destination.LocalDestinationMBean#getSize()
*/
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.ffmq3.local.destination.LocalDestinationMBean#resetStats()
*/
public void resetStats()
{
sentToQueueCount = 0;
receivedFromQueueCount = 0;
acknowledgedGetCount = 0;
rollbackedGetCount = 0;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString()
{
StringBuffer sb = new StringBuffer();
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("]");
return sb.toString();
}
/*
* (non-Javadoc)
* @see net.timewalker.ffmq3.local.destination.AbstractLocalDestination#close()
*/
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();
}
}
}
/*
* (non-Javadoc)
* @see net.timewalker.ffmq3.local.destination.LocalQueueMBean#getSentToQueueCount()
*/
public long getSentToQueueCount()
{
return sentToQueueCount;
}
/*
* (non-Javadoc)
* @see net.timewalker.ffmq3.local.destination.LocalQueueMBean#getReceivedFromQueueCount()
*/
public long getReceivedFromQueueCount()
{
return receivedFromQueueCount;
}
/*
* (non-Javadoc)
* @see net.timewalker.ffmq3.local.destination.LocalQueueMBean#getAcknowledgedGetCount()
*/
public long getAcknowledgedGetCount()
{
return acknowledgedGetCount;
}
/*
* (non-Javadoc)
* @see net.timewalker.ffmq3.local.destination.LocalQueueMBean#getRollbackedGetCount()
*/
public long getRollbackedGetCount()
{
return rollbackedGetCount;
}
/* (non-Javadoc)
* @see net.timewalker.ffmq3.utils.watchdog.ActiveObject#getLastActivity()
*/
public long getLastActivity()
{
return lastActivity;
}
/* (non-Javadoc)
* @see net.timewalker.ffmq3.utils.watchdog.ActiveObject#getTimeoutDelay()
*/
public long getTimeoutDelay()
{
return inactivityTimeout;
}
/* (non-Javadoc)
* @see net.timewalker.ffmq3.utils.watchdog.ActiveObject#onActivityTimeout()
*/
public boolean onActivityTimeout() throws Exception
{
if (closed)
return true;
if (getSize() == 0)
return false; // Queue is empty
// Notify next consumer
messageAvailable(null);
return false;
}
//-------------------------------------------------------------------------------
private 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()
*/
public void run()
{
redeliverMessage(msg,store,handle);
}
}
}