/**
*
* Copyright 2004 Protique Ltd
* Copyright 2005 Hiram Chirino
*
* 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.activemq.service.boundedvm;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jms.JMSException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.broker.BrokerClient;
import org.activemq.filter.AndFilter;
import org.activemq.filter.DestinationMap;
import org.activemq.filter.Filter;
import org.activemq.filter.FilterFactory;
import org.activemq.filter.FilterFactoryImpl;
import org.activemq.filter.NoLocalFilter;
import org.activemq.io.util.MemoryBoundedQueueManager;
import org.activemq.message.ActiveMQDestination;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.ActiveMQQueue;
import org.activemq.message.ConsumerInfo;
import org.activemq.message.MessageAck;
import org.activemq.service.DeadLetterPolicy;
import org.activemq.service.MessageContainer;
import org.activemq.service.MessageContainerManager;
import org.activemq.service.RedeliveryPolicy;
import org.activemq.service.TransactionManager;
import org.activemq.service.TransactionTask;
import org.activemq.store.MessageStore;
import org.activemq.store.PersistenceAdapter;
import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
import EDU.oswego.cs.dl.util.concurrent.Executor;
import EDU.oswego.cs.dl.util.concurrent.PooledExecutor;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;
import EDU.oswego.cs.dl.util.concurrent.ThreadFactory;
/**
* A MessageContainerManager for Durable queues
*
* @version $Revision: 1.1.1.1 $
*/
public class DurableQueueBoundedMessageManager implements MessageContainerManager, Runnable {
private static final int DEFAULT_GARBAGE_COLLECTION_CAPACITY_LIMIT = 10;
private static final long DEFAULT_INACTIVE_TIMEOUT = 30 * 1000;
private static final Log log = LogFactory.getLog(DurableQueueBoundedMessageManager.class);
private MemoryBoundedQueueManager queueManager;
private ConcurrentHashMap containers;
private ConcurrentHashMap subscriptions;
private FilterFactory filterFactory;
private SynchronizedBoolean started;
private SynchronizedBoolean doingGarbageCollection;
private Map destinations;
private DestinationMap destinationMap;
private PooledExecutor threadPool;
private long inactiveTimeout;
private int garbageCoolectionCapacityLimit;
private RedeliveryPolicy redeliveryPolicy;
private DeadLetterPolicy deadLetterPolicy;
private final PersistenceAdapter persistenceAdapter;
protected static class DurableQueueThreadFactory implements ThreadFactory {
/**
* @param command - command to run
* @return a new thread
*/
public Thread newThread(Runnable command) {
Thread result = new Thread(command, "Durable Queue Worker");
result.setPriority(Thread.NORM_PRIORITY + 1);
result.setDaemon(true);
return result;
}
}
/**
* Constructor for DurableQueueBoundedMessageManager
*
* @param mgr
* @param redeliveryPolicy
* @param deadLetterPolicy
*/
public DurableQueueBoundedMessageManager(PersistenceAdapter persistenceAdapter, MemoryBoundedQueueManager mgr, RedeliveryPolicy redeliveryPolicy,
DeadLetterPolicy deadLetterPolicy) {
this.persistenceAdapter = persistenceAdapter;
this.queueManager = mgr;
this.redeliveryPolicy = redeliveryPolicy;
this.deadLetterPolicy = deadLetterPolicy;
this.containers = new ConcurrentHashMap();
this.destinationMap = new DestinationMap();
this.destinations = new ConcurrentHashMap();
this.subscriptions = new ConcurrentHashMap();
this.filterFactory = new FilterFactoryImpl();
this.started = new SynchronizedBoolean(false);
this.doingGarbageCollection = new SynchronizedBoolean(false);
this.threadPool = new PooledExecutor();
this.threadPool.setThreadFactory(new DurableQueueThreadFactory());
this.inactiveTimeout = DEFAULT_INACTIVE_TIMEOUT;
this.garbageCoolectionCapacityLimit = DEFAULT_GARBAGE_COLLECTION_CAPACITY_LIMIT;
}
/**
* @return Returns the garbageCoolectionCapacityLimit.
*/
public int getGarbageCoolectionCapacityLimit() {
return garbageCoolectionCapacityLimit;
}
/**
* @param garbageCoolectionCapacityLimit The garbageCoolectionCapacityLimit to set.
*/
public void setGarbageCoolectionCapacityLimit(int garbageCoolectionCapacityLimit) {
this.garbageCoolectionCapacityLimit = garbageCoolectionCapacityLimit;
}
/**
* @return Returns the inactiveTimeout.
*/
public long getInactiveTimeout() {
return inactiveTimeout;
}
/**
* @param inactiveTimeout The inactiveTimeout to set.
*/
public void setInactiveTimeout(long inactiveTimeout) {
this.inactiveTimeout = inactiveTimeout;
}
/**
* start the manager
*
* @throws JMSException
*/
public void start() throws JMSException {
if (started.commit(false, true)) {
for (Iterator i = containers.values().iterator();i.hasNext();) {
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
container.start();
}
try {
threadPool.execute(this);//start garbage collection
}
catch (InterruptedException e) {
JMSException jmsEx = new JMSException("Garbage collection interupted on start()");
jmsEx.setLinkedException(e);
throw jmsEx;
}
}
}
/**
* stop the manager
*
* @throws JMSException
*/
public void stop() throws JMSException {
if (started.commit(true, false)) {
for (Iterator i = containers.values().iterator();i.hasNext();) {
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
container.stop();
}
threadPool.interruptAll();
threadPool.shutdownNow();
}
}
/**
* collect expired messages
*/
public void run() {
while (started.get()) {
doGarbageCollection();
try {
Thread.sleep(2000);
}
catch (InterruptedException e) {
}
}
}
/**
* Add a consumer if appropiate
*
* @param client
* @param info
* @throws JMSException
*/
public synchronized void addMessageConsumer(BrokerClient client, ConsumerInfo info) throws JMSException {
ActiveMQDestination destination = info.getDestination();
if ( !destination.isQueue() || destination.isTemporary() )
return;
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) containers
.get(destination);
if (container == null) {
container = createContainer(destination,false);
}
if (log.isDebugEnabled()) {
log.debug("Adding consumer: " + info);
}
DurableQueueSubscription ts = container.addConsumer(createFilter(info), info, client);
if (ts != null) {
subscriptions.put(info.getConsumerId(), ts);
}
String name = destination.getPhysicalName();
if (!destinations.containsKey(name)) {
destinations.put(name, destination);
}
}
/**
* @param client
* @param destination
* @param isDeadLetterQueue is this queue a dead letter queue
* @return the container
* @throws JMSException
*/
private DurableQueueBoundedMessageContainer createContainer(ActiveMQDestination destination, boolean isDeadLetterQueue) throws JMSException {
MessageStore messageStore = persistenceAdapter.createQueueMessageStore(destination.getPhysicalName());
DurableQueueBoundedMessageContainer container = new DurableQueueBoundedMessageContainer(messageStore, threadPool, queueManager, destination, isDeadLetterQueue ? null : redeliveryPolicy, deadLetterPolicy);
addContainer(container);
if (started.get()) {
container.start();
}
return container;
}
/**
* @param client
* @param info
* @throws JMSException
*/
public synchronized void removeMessageConsumer(BrokerClient client, ConsumerInfo info) throws JMSException {
ActiveMQDestination destination = info.getDestination();
if ( !destination.isQueue() || destination.isTemporary() )
return;
for (Iterator i = containers.values().iterator();i.hasNext();) {
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
if (container != null) {
container.removeConsumer(info);
}
}
subscriptions.remove(info.getConsumerId());
}
/**
* Delete a durable subscriber
*
* @param clientId
* @param subscriberName
* @throws JMSException if the subscriber doesn't exist or is still active
*/
public void deleteSubscription(String clientId, String subscriberName) throws JMSException {
}
/**
* @param client
* @param message
* @throws JMSException
*/
public void sendMessage(final BrokerClient client, final ActiveMQMessage message) throws JMSException {
ActiveMQDestination destination = message.getJMSActiveMQDestination();
if (!destination.isQueue() || destination.isTemporary() || !message.isPersistent())
return;
if (queueManager.getCurrentCapacity() <= garbageCoolectionCapacityLimit) {
doGarbageCollection();
}
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) containers.get(destination);
if (container == null) {
container = createContainer(destination, false);
}
Set set = destinationMap.get(message.getJMSActiveMQDestination());
for (Iterator i = set.iterator();i.hasNext();) {
container = (DurableQueueBoundedMessageContainer) i.next();
container.enqueue(message);
}
}
/**
* @param client
* @param ack
* @throws JMSException
*/
public void acknowledgeMessage(final BrokerClient client, final MessageAck ack) throws JMSException {
ActiveMQDestination destination = ack.getDestination();
if (destination == null) {
log.warn("Ignoring acknowledgeMessage() on null destination: " + ack);
return;
}
if (!destination.isQueue() || destination.isTemporary() || !ack.isPersistent())
return;
final DurableQueueSubscription ts = (DurableQueueSubscription) subscriptions.get(ack.getConsumerId());
if (ts == null)
return;
final DurableMessagePointer messagePointer = ts.acknowledgeMessage(ack.getMessageID());
if (messagePointer == null )
return;
if( ts.isBrowser() ) {
ts.addAckedMessage(messagePointer);
return;
}
if (!ack.isMessageRead() || ack.isExpired()) {
redeliverMessage(ts, ack, messagePointer);
return;
}
// Let the message store ack the message.
messagePointer.getMessageStore().removeMessage(ack);
if (TransactionManager.isCurrentTransaction()) {
// Hook in a callback on first acked message
if (!ts.hasAckedMessage()) {
TransactionManager.getContexTransaction().addPostRollbackTask(new TransactionTask() {
public void execute() throws Throwable {
List ackList = ts.listAckedMessages();
HashMap redeliverMap = new HashMap();
//for (int x = ackList.size()-1; x >= 0 ; x--){
for (int x = 0; x < ackList.size(); x++){
DurableMessagePointer messagePointer = (DurableMessagePointer) ackList.get(x);
ActiveMQMessage message = messagePointer.getMessage();
message.setJMSRedelivered(true);
if (message.incrementDeliveryCount() >= redeliveryPolicy.getMaximumRetryCount()) {
if (log.isDebugEnabled()){
log.debug("Message: " + message + " has exceeded its retry count");
}
// TODO: see if we can use the deadLetterPolicy of the container that dispatched the message.
deadLetterPolicy.sendToDeadLetter(message);
}
else if (ack.isExpired()) {
if (log.isDebugEnabled()){
log.debug("Message: " + message + " has expired");
}
// TODO: see if we can use the deadLetterPolicy of the container that dispatched the message.
deadLetterPolicy.sendToDeadLetter(message);
}
else {
Set containers = destinationMap.get(message.getJMSActiveMQDestination());
for (Iterator i = containers.iterator();i.hasNext();) {
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
LinkedList l = (LinkedList) redeliverMap.get(container);
if( l==null ) {
l = new LinkedList();
redeliverMap.put(container, l);
}
l.add(messagePointer);
}
}
}
for (Iterator i = redeliverMap.keySet().iterator();i.hasNext();) {
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
List l = (List) redeliverMap.get(container);
container.redeliver(l);
}
ts.removeAllAckedMessages();
}
});
TransactionManager.getContexTransaction().addPostCommitTask(new TransactionTask() {
public void execute() throws Throwable {
ts.removeAllAckedMessages();
}
});
}
// We need to keep track of messages that were acked. If we
// rollback.
ts.addAckedMessage(messagePointer);
}
}
/**
* @param client
* @param ack
* @param message
* @throws JMSException
*/
private void redeliverMessage(DurableQueueSubscription ts, MessageAck ack, DurableMessagePointer message) throws JMSException {
message.getMessage().setJMSRedelivered(true);
if (message.incrementDeliveryCount() >= redeliveryPolicy.getMaximumRetryCount()) {
if (log.isDebugEnabled()){
log.debug("Message: " + message + " has exceeded its retry count");
}
deadLetterPolicy.sendToDeadLetter(message.getMessage());
}
else if (ack.isExpired()) {
if (log.isDebugEnabled()){
log.debug("Message: " + message + " has expired");
}
deadLetterPolicy.sendToDeadLetter(message.getMessage());
}
else {
Set set = destinationMap.get(message.getMessage().getJMSActiveMQDestination());
for (Iterator i = set.iterator();i.hasNext();) {
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
container.redeliver(message);
break;
}
}
}
/**
* @throws JMSException
*/
public void poll() throws JMSException {
}
/**
* @param physicalName
* @return MessageContainer
* @throws JMSException
*/
public MessageContainer getContainer(String physicalName) throws JMSException {
ActiveMQDestination key = (ActiveMQDestination) destinations.get(physicalName);
if (key != null) {
return (MessageContainer) containers.get(key);
}
return null;
}
/**
* @return a map of destinations
*/
public Map getDestinations() {
return Collections.unmodifiableMap(containers);
}
/**
* Returns an unmodifiable map, indexed by String name, of all the {@link javax.jms.Destination}
* objects used by non-broker consumers directly connected to this container
*
* @return
*/
public Map getLocalDestinations() {
Map localDestinations = new HashMap();
for (Iterator iter = subscriptions.values().iterator(); iter.hasNext();) {
DurableSubscription sub = (DurableSubscription) iter.next();
if (sub.isLocalSubscription()) {
final ActiveMQDestination dest = sub.getDestination();
localDestinations.put(dest.getPhysicalName(), dest);
}
}
return Collections.unmodifiableMap(localDestinations);
}
/**
* @return the DeadLetterPolicy for this Container Manager
*/
public DeadLetterPolicy getDeadLetterPolicy() {
return deadLetterPolicy;
}
/**
* Set the DeadLetterPolicy for this Container Manager
*
* @param policy
*/
public void setDeadLetterPolicy(DeadLetterPolicy policy) {
this.deadLetterPolicy = policy;
}
/**
* Create filter for a Consumer
*
* @param info
* @return the Fitler
* @throws javax.jms.JMSException
*/
protected Filter createFilter(ConsumerInfo info) throws JMSException {
Filter filter = filterFactory.createFilter(info.getDestination(), info.getSelector());
if (info.isNoLocal()) {
filter = new AndFilter(filter, new NoLocalFilter(info.getClientId()));
}
return filter;
}
private void doGarbageCollection() {
if (doingGarbageCollection.commit(true, false)) {
if (queueManager.getCurrentCapacity() <= garbageCoolectionCapacityLimit) {
for (Iterator i = containers.values().iterator();i.hasNext();) {
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
container.removeExpiredMessages();
log.warn("memory limit low - forced to remove expired messages: "
+ container.getDestinationName());
}
}
//if still below the limit - clear queues with no subscribers
//which have been inactive for a while
if (queueManager.getCurrentCapacity() <= garbageCoolectionCapacityLimit) {
for (Iterator i = containers.values().iterator();i.hasNext();) {
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
if (!container.isActive() && (container.getIdleTimestamp() < (System.currentTimeMillis() - inactiveTimeout))) {
removeContainer(container);
log.warn("memory limit low - forced to remove inactive and idle queue: "
+ container.getDestinationName());
}
}
}
//if still now below limit - clear inactive queues
if (queueManager.getCurrentCapacity() <= garbageCoolectionCapacityLimit) {
for (Iterator i = containers.values().iterator();i.hasNext();) {
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
if (!container.isActive() && !container.isEmpty()) {
removeContainer(container);
log.warn("memory limit low - forced to remove inactive queue: "
+ container.getDestinationName());
}
}
}
doingGarbageCollection.set(false);
}
}
private synchronized void addContainer(DurableQueueBoundedMessageContainer container) {
containers.put(container.getDestination(), container);
destinationMap.put(container.getDestination(), container);
}
private synchronized void removeContainer(DurableQueueBoundedMessageContainer container) {
try {
container.close();
log.info("closed inactive Durable queue container: " + container.getDestinationName());
}
catch (JMSException e) {
log.warn("failure closing container", e);
}
containers.remove(container.getDestination());
destinationMap.remove(container.getDestination(), container);
}
protected Executor getThreadPool() {
return threadPool;
}
public void createMessageContainer(ActiveMQDestination dest) throws JMSException {
createContainer(dest, false);
}
public Map getMessageContainerAdmins() throws JMSException {
return Collections.unmodifiableMap(containers);
}
public void destroyMessageContainer(ActiveMQDestination dest) throws JMSException {
if ( !dest.isQueue() )
return;
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) containers.remove(dest);
if( container != null ) {
container.empty();
container.stop();
}
destinationMap.removeAll(dest);
}
public void sendToDeadLetterQueue(String deadLetterName, ActiveMQMessage message) throws JMSException {
ActiveMQQueue destination = new ActiveMQQueue(deadLetterName);
DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) containers.get(destination);
if (container == null) {
container = createContainer(destination, true);
}
container.enqueue(message);
}
}