net.timewalker.ffmq4.local.destination.LocalQueue Maven / Gradle / Ivy
/*
* 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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.SynchronizationBarrier;
import net.timewalker.ffmq4.utils.concurrent.WaitTimeoutException;
import net.timewalker.ffmq4.utils.watchdog.ActiveObject;
import net.timewalker.ffmq4.utils.watchdog.ActivityWatchdog;
/**
* Implementation for a local JMS {@link Queue}
*/
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();
private AtomicLong storeFullEventsCount = 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 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)
{
storeFullEventsCount.incrementAndGet();
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 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 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 )
{
consumersLock.readLock().lock();
try
{
switch (localConsumers.size())
{
case 0 : return; // Nobody's listening
case 1 :
notifySingleConsumer(localConsumers.get(0),message);
break;
default : // Multiple consumers
notifyNextConsumer(localConsumers,message);
break;
}
}
finally
{
consumersLock.readLock().unlock();
}
}
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( List allConsumers , AbstractMessage message )
{
// Find a consumer to notify
int localConsumersCount = allConsumers.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 = allConsumers.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);
storeFullEventsCount.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(",storeFullEvents=");
sb.append(storeFullEventsCount);
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();
}
}
// Create a snapshot to avoid concurrent modification
List consumers;
consumersLock.readLock().lock();
try
{
if (localConsumers.isEmpty())
return;
consumers = new ArrayList<>(localConsumers);
}
finally
{
consumersLock.readLock().unlock();
}
// Close all consumers
for (int n=0;n
© 2015 - 2024 Weber Informatics LLC | Privacy Policy