/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.qpid.server.queue;
import org.apache.log4j.Logger;
import org.apache.qpid.AMQException;
import org.apache.qpid.util.ConcurrentLinkedQueueAtomicSize;
import org.apache.qpid.configuration.Configured;
import org.apache.qpid.framing.ContentBody;
import org.apache.qpid.server.configuration.Configurator;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Manages delivery of messages on behalf of a queue
*/
public class ConcurrentDeliveryManager implements DeliveryManager
{
private static final Logger _log = Logger.getLogger(ConcurrentDeliveryManager.class);
@Configured(path = "advanced.compressBufferOnQueue",
defaultValue = "false")
public boolean compressBufferOnQueue;
/**
* Holds any queued messages
*/
private final Queue<AMQMessage> _messages = new ConcurrentLinkedQueueAtomicSize<AMQMessage>();
//private int _messageCount;
/**
* Ensures that only one asynchronous task is running for this manager at
* any time.
*/
private final AtomicBoolean _processing = new AtomicBoolean();
/**
* The subscriptions on the queue to whom messages are delivered
*/
private final SubscriptionManager _subscriptions;
/**
* A reference to the queue we are delivering messages for. We need this to be able
* to pass the code that handles acknowledgements a handle on the queue.
*/
private final AMQQueue _queue;
/**
* Lock used to ensure that an channel that becomes unsuspended during the start of the queueing process is forced
* to wait till the first message is added to the queue. This will ensure that the _queue has messages to be delivered
* via the async thread.
* <p/>
* Lock is used to control access to hasQueuedMessages() and over the addition of messages to the queue.
*/
private ReentrantLock _lock = new ReentrantLock();
ConcurrentDeliveryManager(SubscriptionManager subscriptions, AMQQueue queue)
{
//Set values from configuration
Configurator.configure(this);
if (compressBufferOnQueue)
{
_log.info("Compressing Buffers on queue.");
}
_subscriptions = subscriptions;
_queue = queue;
}
/**
* @return boolean if we are queueing
*/
private boolean queueing()
{
return hasQueuedMessages();
}
/**
* @param msg to enqueue
* @return true if we are queue this message
*/
private boolean enqueue(AMQMessage msg)
{
if (msg.isImmediate())
{
return false;
}
else
{
_lock.lock();
try
{
if (queueing())
{
return addMessageToQueue(msg);
}
else
{
return false;
}
}
finally
{
_lock.unlock();
}
}
}
private void startQueueing(AMQMessage msg)
{
if (!msg.isImmediate())
{
addMessageToQueue(msg);
}
}
private boolean addMessageToQueue(AMQMessage msg)
{
// Shrink the ContentBodies to their actual size to save memory.
if (compressBufferOnQueue)
{
Iterator it = msg.getContentBodies().iterator();
while (it.hasNext())
{
ContentBody cb = (ContentBody) it.next();
cb.reduceBufferToFit();
}
}
_messages.offer(msg);
return true;
}
public boolean hasQueuedMessages()
{
_lock.lock();
try
{
return !_messages.isEmpty();
}
finally
{
_lock.unlock();
}
}
public int getQueueMessageCount()
{
return getMessageCount();
}
/**
* This is an EXPENSIVE opperation to perform with a ConcurrentLinkedQueue as it must run the queue to determine size.
* The ConcurrentLinkedQueueAtomicSize uses an AtomicInteger to record the number of elements on the queue.
*
* @return int the number of messages in the delivery queue.
*/
private int getMessageCount()
{
return _messages.size();
}
public synchronized List<AMQMessage> getMessages()
{
return new ArrayList<AMQMessage>(_messages);
}
public synchronized void removeAMessageFromTop() throws AMQException
{
AMQMessage msg = poll();
if (msg != null)
{
msg.dequeue(_queue);
}
}
public synchronized void clearAllMessages() throws AMQException
{
AMQMessage msg = poll();
while (msg != null)
{
msg.dequeue(_queue);
msg = poll();
}
}
/**
* Only one thread should ever execute this method concurrently, but
* it can do so while other threads invoke deliver().
*/
private void processQueue()
{
try
{
boolean hasSubscribers = _subscriptions.hasActiveSubscribers();
AMQMessage message = peek();
//While we have messages to send and subscribers to send them to.
while (message != null && hasSubscribers)
{
// _log.debug("Have messages(" + _messages.size() + ") and subscribers");
Subscription next = _subscriptions.nextSubscriber(message);
//FIXME Is there still not the chance that this subscribe could be suspended between here and the send?
//We don't synchronize access to subscribers so need to re-check
if (next != null)
{
next.send(message, _queue);
poll();
message = peek();
}
else
{
hasSubscribers = false;
}
}
}
catch (FailedDequeueException e)
{
_log.error("Unable to deliver message as dequeue failed: " + e, e);
}
finally
{
_log.debug("End of processQueue: (" + getQueueMessageCount() + ")" + " subscribers:" + _subscriptions.hasActiveSubscribers());
}
}
private AMQMessage peek()
{
return _messages.peek();
}
private AMQMessage poll()
{
return _messages.poll();
}
Runner asyncDelivery = new Runner();
public void processAsync(Executor executor)
{
_log.debug("Processing Async. Queued:" + hasQueuedMessages() + "(" + getQueueMessageCount() + ")" +
" Active:" + _subscriptions.hasActiveSubscribers() +
" Processing:" + _processing.get());
if (hasQueuedMessages() && _subscriptions.hasActiveSubscribers())
{
//are we already running? if so, don't re-run
if (_processing.compareAndSet(false, true))
{
executor.execute(asyncDelivery);
}
}
}
public void deliver(String name, AMQMessage msg) throws FailedDequeueException
{
// first check whether we are queueing, and enqueue if we are
if (!enqueue(msg))
{
// not queueing so deliver message to 'next' subscriber
_lock.lock();
try
{
Subscription s = _subscriptions.nextSubscriber(msg);
if (s == null)
{
if (!msg.isImmediate())
{
// no subscribers yet so enter 'queueing' mode and queue this message
startQueueing(msg);
}
}
else
{
s.send(msg, _queue);
msg.setDeliveredToConsumer();
}
}
finally
{
_lock.unlock();
}
}
}
private class Runner implements Runnable
{
public void run()
{
boolean running = true;
while (running)
{
processQueue();
//Check that messages have not been added since we did our last peek();
// Synchronize with the thread that adds to the queue.
// If the queue is still empty then we can exit
_lock.lock();
try
{
if (!(hasQueuedMessages() && _subscriptions.hasActiveSubscribers()))
{
running = false;
_processing.set(false);
}
}
finally
{
_lock.unlock();
}
}
}
}
}