Package org.jboss.jms.server.endpoint

Source Code of org.jboss.jms.server.endpoint.ServerSessionEndpoint

/*
  * JBoss, Home of Professional Open Source
  * Copyright 2005, JBoss Inc., and individual contributors as indicated
  * by the @authors tag. See the copyright.txt in the distribution for a
  * full listing of individual contributors.
  *
  * This 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.1 of
  * the License, or (at your option) any later version.
  *
  * This software 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 this software; if not, write to the Free
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
package org.jboss.jms.server.endpoint;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;

import javax.jms.IllegalStateException;
import javax.jms.InvalidDestinationException;
import javax.jms.JMSException;

import org.jboss.aop.AspectManager;
import org.jboss.jms.client.delegate.ClientBrowserDelegate;
import org.jboss.jms.client.delegate.ClientConsumerDelegate;
import org.jboss.jms.delegate.Ack;
import org.jboss.jms.delegate.BrowserDelegate;
import org.jboss.jms.delegate.Cancel;
import org.jboss.jms.delegate.ConsumerDelegate;
import org.jboss.jms.delegate.DeliveryInfo;
import org.jboss.jms.delegate.DeliveryRecovery;
import org.jboss.jms.destination.JBossDestination;
import org.jboss.jms.destination.JBossQueue;
import org.jboss.jms.destination.JBossTopic;
import org.jboss.jms.message.JBossMessage;
import org.jboss.jms.server.DestinationManager;
import org.jboss.jms.server.JMSCondition;
import org.jboss.jms.server.ServerPeer;
import org.jboss.jms.server.destination.ManagedDestination;
import org.jboss.jms.server.destination.ManagedQueue;
import org.jboss.jms.server.destination.ManagedTopic;
import org.jboss.jms.server.destination.TopicService;
import org.jboss.jms.server.endpoint.advised.BrowserAdvised;
import org.jboss.jms.server.endpoint.advised.ConsumerAdvised;
import org.jboss.jms.server.messagecounter.MessageCounter;
import org.jboss.jms.server.selector.Selector;
import org.jboss.jms.wireformat.ClientDelivery;
import org.jboss.jms.wireformat.Dispatcher;
import org.jboss.logging.Logger;
import org.jboss.messaging.core.contract.Binding;
import org.jboss.messaging.core.contract.Channel;
import org.jboss.messaging.core.contract.Condition;
import org.jboss.messaging.core.contract.Delivery;
import org.jboss.messaging.core.contract.DeliveryObserver;
import org.jboss.messaging.core.contract.Message;
import org.jboss.messaging.core.contract.MessageReference;
import org.jboss.messaging.core.contract.MessageStore;
import org.jboss.messaging.core.contract.PersistenceManager;
import org.jboss.messaging.core.contract.PostOffice;
import org.jboss.messaging.core.contract.Queue;
import org.jboss.messaging.core.contract.Replicator;
import org.jboss.messaging.core.impl.IDManager;
import org.jboss.messaging.core.impl.MessagingQueue;
import org.jboss.messaging.core.impl.tx.Transaction;
import org.jboss.messaging.core.impl.tx.TransactionException;
import org.jboss.messaging.core.impl.tx.TransactionRepository;
import org.jboss.messaging.core.impl.tx.TxCallback;
import org.jboss.messaging.util.CompatibleExecutor;
import org.jboss.messaging.util.ExceptionUtil;
import org.jboss.messaging.util.ExecutorFactory;
import org.jboss.messaging.util.GUIDGenerator;
import org.jboss.messaging.util.JBMThreadFactory;
import org.jboss.messaging.util.MessageQueueNameHelper;
import org.jboss.messaging.util.OrderedExecutorFactory;
import org.jboss.remoting.callback.Callback;
import org.jboss.remoting.callback.ServerInvokerCallbackHandler;

import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
import EDU.oswego.cs.dl.util.concurrent.LinkedQueue;

/**
* The server side representation of a JMS session.
*
* A user must not invoke methods of a session concurrently on different threads, however there are
* situations where multiple threads may access this object concurrently, for instance:
* - A session can be closed when it's connection is closed by the user which might be called on a
*   different thread.
* - A session can be closed when the server determines the connection is dead.
*
* If the session represents a connection consumer's session then the connection consumer will farm
* off messages to different sessions obtained from a pool, these may then cancel/ack etc on
* different threads, but the acks/cancels/etc will end up back here on the connection consumer
* session instance.
*
* @author <a href="mailto:ovidiu@feodorov.com">Ovidiu Feodorov</a>
* @author <a href="mailto:tim.fox@jboss.com">Tim Fox</a>
* @author <a href="mailto:clebert.suconic@jboss.com">Clebert Suconic</a>
* @version <tt>$Revision: 6823 $</tt>
*
* $Id: ServerSessionEndpoint.java 6823 2009-05-18 07:17:01Z gaohoward $
*/
public class ServerSessionEndpoint implements SessionInternalEndpoint
{
   // Constants ------------------------------------------------------------------------------------

   private static final Logger log = Logger.getLogger(ServerSessionEndpoint.class);

   static final String DUR_SUB_STATE_CONSUMERS = "C";

   static final String TEMP_QUEUE_MESSAGECOUNTER_PREFIX = "TempQueue.";

   private static final long DELIVERY_WAIT_TIMEOUT = 5 * 1000;

   private static final long CLOSE_WAIT_TIMEOUT = 10 * 1000;

   // Static ---------------------------------------------------------------------------------------


   private static final ExecutorFactory executorFactory = new OrderedExecutorFactory(
           Executors.newCachedThreadPool(new JBMThreadFactory("server-session-endpoint")));
   // Attributes -----------------------------------------------------------------------------------

   private boolean trace = log.isTraceEnabled();
  
   private boolean debug = log.isDebugEnabled();

   private String id;

   private volatile boolean closed;

   private ServerConnectionEndpoint connectionEndpoint;
   private ServerInvokerCallbackHandler callbackHandler;

   private ServerPeer sp;

   private Map consumers;
   private Map browsers;

   private PersistenceManager pm;
   private MessageStore ms;

   private DestinationManager dm;
   private IDManager idm;
   private TransactionRepository tr;
   private PostOffice postOffice;
   private int nodeId;
   private int defaultMaxDeliveryAttempts;
   private long defaultRedeliveryDelay;
   private Queue defaultDLQ;
   private Queue defaultExpiryQueue;
   private boolean supportsFailover;
   private boolean replicating;

   private Object deliveryLock = new Object();

   // Map <deliveryID, Delivery>
   private Map deliveries;

   private long deliveryIdSequence;

   //Temporary until we have our own NIO transport
   CompatibleExecutor executor;

   private LinkedQueue toDeliver = new LinkedQueue();

   private boolean waitingToClose = false;

   private Object waitLock = new Object();

   private long lastSequence = -1;


   // Constructors ---------------------------------------------------------------------------------

   ServerSessionEndpoint(String sessionID, ServerConnectionEndpoint connectionEndpoint,
                       boolean replicating) throws Exception
   {
      this.id = sessionID;
     
      this.executor = executorFactory.getExecutor("jbm-server-session-" + sessionID);

      this.connectionEndpoint = connectionEndpoint;

      this.replicating = replicating;

      callbackHandler = connectionEndpoint.getCallbackHandler();

      sp = connectionEndpoint.getServerPeer();

      pm = sp.getPersistenceManagerInstance();

      ms = sp.getMessageStore();

      dm = sp.getDestinationManager();

      postOffice = sp.getPostOfficeInstance();

      supportsFailover = connectionEndpoint.getConnectionFactoryEndpoint().isSupportsFailover() && postOffice.isClustered();

      idm = sp.getChannelIDManager();

      nodeId = sp.getServerPeerID();

      tr = sp.getTxRepository();

      consumers = new HashMap();

    browsers = new HashMap();

      defaultDLQ = sp.getDefaultDLQInstance();

      defaultExpiryQueue = sp.getDefaultExpiryQueueInstance();

      tr = sp.getTxRepository();

      defaultMaxDeliveryAttempts = sp.getDefaultMaxDeliveryAttempts();

      defaultRedeliveryDelay = sp.getDefaultRedeliveryDelay();

      deliveries = new ConcurrentHashMap();
   }

   // SessionDelegate implementation ---------------------------------------------------------------

   public ConsumerDelegate createConsumerDelegate(JBossDestination jmsDestination,
                                                  String selector,
                                                  boolean noLocal,
                                                  String subscriptionName,
                                                  boolean isCC,
                                                  boolean autoFlowControl) throws JMSException
   {
      try
      {
        //TODO This is a temporary kludge to allow creation of consumers directly on core queues for
        //cluster connections
        //This will disappear once we move all JMS knowledge to the client side

        if (jmsDestination.isDirect())
        {
          return createConsumerDelegateDirect(jmsDestination.getName(), selector);
        }
        else
        {
           return createConsumerDelegateInternal(jmsDestination, selector,
                                                 noLocal, subscriptionName);
        }
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " createConsumerDelegate");
      }
   }

  public BrowserDelegate createBrowserDelegate(JBossDestination jmsDestination,
                                                String selector)
      throws JMSException
  {
      try
      {
         return createBrowserDelegateInternal(jmsDestination, selector);
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " createBrowserDelegate");
      }
  }

   public JBossQueue createQueue(String name) throws JMSException
   {
      try
      {
         if (closed)
         {
            throw new IllegalStateException("Session is closed");
         }

         ManagedDestination dest = (ManagedDestination)dm.getDestination(name, true);

         if (dest == null)
         {
            throw new JMSException("There is no administratively defined queue with name:" + name);
         }

         return new JBossQueue(dest.getName());
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " createQueue");
      }
   }

   public JBossTopic createTopic(String name) throws JMSException
   {
      try
      {
         if (closed)
         {
            throw new IllegalStateException("Session is closed");
         }

         ManagedDestination dest = (ManagedDestination)dm.getDestination(name, false);

         if (dest == null)
         {
            throw new JMSException("There is no administratively defined topic with name:" + name);
         }

         return new JBossTopic(name);
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " createTopic");
      }
   }

   public void close() throws JMSException
   {
      try
      {
         localClose();

         connectionEndpoint.removeSession(id);
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " close");
      }
   }

   public long closing(long sequence) throws JMSException
   {
      if (trace) log.trace(this + " closing");

      //Need to wait for messages to arrive or they may be lost if connection/session is closed too
      //quickly after sending

      if (sequence != -1)
      {
         synchronized (waitLock)
         {
            long wait = CLOSE_WAIT_TIMEOUT;

            while (lastSequence != sequence - 1 && wait > 0)
            {
               long start = System.currentTimeMillis();
               try
               {
                  waitLock.wait(wait);
               }
               catch (InterruptedException e)
               {
               }
               wait -= (System.currentTimeMillis() - start);
            }

            if (wait <= 0)
            {
               log.warn("Timed out waiting for last message");
            }
         }
      }

      return -1;
   }

   public void send(JBossMessage message, final boolean checkForDuplicates) throws JMSException
   {
      throw new IllegalStateException("Should not be handled here");
   }

   public void send(JBossMessage message, final boolean checkForDuplicates, long sequence) throws JMSException
   {
      try
      {
         if (checkForDuplicates && !message.isReliable())
         {
            //Ignore np messages on failover
            return;
         }

         connectionEndpoint.sendMessage(message, null, checkForDuplicates);

         if (sequence != -1)
         {
            synchronized (waitLock)
            {
               this.lastSequence = sequence;

               waitLock.notifyAll();
            }
         }
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " send");
      }
   }

   public boolean acknowledgeDelivery(Ack ack) throws JMSException
   {
      try
      {
         return acknowledgeDeliveryInternal(ack);
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " acknowledgeDelivery");
      }
   }

   public void acknowledgeDeliveries(List acks) throws JMSException
   {
      if (trace) {log.trace(this + " acknowledges deliveries " + acks); }

      try
      {
         Iterator iter = acks.iterator();

         while (iter.hasNext())
         {
            Ack ack = (Ack)iter.next();

            acknowledgeDeliveryInternal(ack);
         }
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " acknowledgeDeliveries");
      }
   }

   public void cancelDelivery(Cancel cancel) throws JMSException
   {
      if (trace) {log.trace(this + " cancelDelivery " + cancel); }

      try
      {
         Delivery del = cancelDeliveryInternal(cancel);

         if (del != null)
         {
           //Prompt delivery
           promptDelivery((Channel)del.getObserver());
         }
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " cancelDelivery");
      }
   }

   public void cancelDeliveries(List cancels) throws JMSException
   {
      if (trace) {log.trace(this + " cancels deliveries " + cancels); }

      try
      {
         // deliveries must be cancelled in reverse order

         Set channels = new HashSet();

         for (int i = cancels.size() - 1; i >= 0; i--)
         {
            Cancel cancel = (Cancel)cancels.get(i);

            if (trace) { log.trace(this + " cancelling delivery " + cancel.getDeliveryId()); }

            Delivery del = cancelDeliveryInternal(cancel);

            if (del != null)
            {
              channels.add(del.getObserver());
            }
         }

         if (trace) { log.trace("Cancelled deliveries"); }

         // need to prompt delivery for all affected channels

         promptDelivery(channels);
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " cancelDeliveries");
      }
   }

   public void recoverDeliveries(List deliveryRecoveryInfos, String oldSessionID) throws JMSException
   {
      if (trace) { log.trace(this + " recovers deliveries " + deliveryRecoveryInfos); }

      try
      {
         if (!postOffice.isClustered())
         {
            throw new IllegalStateException("Recovering deliveries but post office is not clustered!");
         }

         long maxDeliveryId = 0;

         //Sort into different list for each channel
         Map ackMap = new HashMap();

         for (Iterator iter = deliveryRecoveryInfos.iterator(); iter.hasNext(); )
         {
            DeliveryRecovery deliveryInfo = (DeliveryRecovery)iter.next();

            String queueName = deliveryInfo.getQueueName();

            List acks = (List)ackMap.get(queueName);

            if (acks == null)
            {
               acks = new ArrayList();

               ackMap.put(queueName, acks);
            }

            acks.add(deliveryInfo);
         }

         Iterator iter = ackMap.entrySet().iterator();

         while (iter.hasNext())
         {
            Map.Entry entry = (Map.Entry)iter.next();

            String queueName = (String)entry.getKey();

            //Look up the queue

            Binding binding = postOffice.getBindingForQueueName(queueName);

            Queue queue = binding.queue;

            if (queue == null)
            {
               throw new IllegalStateException("Cannot find queue with queue name: " + queueName);
            }

            List acks = (List)entry.getValue();

            List ids = new ArrayList(acks.size());

            for (Iterator iter2 = acks.iterator(); iter2.hasNext(); )
            {
               DeliveryRecovery info = (DeliveryRecovery)iter2.next();

               ids.add(new Long(info.getMessageID()));
            }

            JMSCondition cond = (JMSCondition)binding.condition;

            ManagedDestination dest =
               sp.getDestinationManager().getDestination(cond.getName(), cond.isQueue());

            if (dest == null)
            {
               throw new IllegalStateException("Cannot find managed destination with name " +
                  cond.getName() + " isQueue" + cond.isQueue());
            }

            Queue dlqToUse =
               dest.getDLQ() == null ? defaultDLQ : dest.getDLQ();

            Queue expiryQueueToUse =
               dest.getExpiryQueue() == null ? defaultExpiryQueue : dest.getExpiryQueue();

            int maxDeliveryAttemptsToUse =
               dest.getMaxDeliveryAttempts() == -1 ? defaultMaxDeliveryAttempts : dest.getMaxDeliveryAttempts();

            List dels = queue.recoverDeliveries(ids);

            Iterator iter2 = dels.iterator();

            Iterator iter3 = acks.iterator();

            while (iter2.hasNext())
            {
               Delivery del = (Delivery)iter2.next();

               DeliveryRecovery info = (DeliveryRecovery)iter3.next();

               long deliveryId = info.getDeliveryID();

               maxDeliveryId = Math.max(maxDeliveryId, deliveryId);

               if (trace) { log.trace(this + " Recovered delivery " + deliveryId + ", " + del); }

               deliveries.put(new Long(deliveryId),
                              new DeliveryRecord(del, dlqToUse, expiryQueueToUse, dest.getRedeliveryDelay(),
                                  maxDeliveryAttemptsToUse, queueName, supportsFailover, deliveryId));

               //We want to replicate the deliveries to the new backup, but we don't want a response since that would cause actual delivery
               //to occur, which we don't want since the client already has the deliveries

               if (supportsFailover)
               {
                 postOffice.sendReplicateDeliveryMessage(queueName, id, del.getReference().getMessage().getMessageID(), deliveryId, false, true);
               }
            }
         }

         iter = postOffice.getAllBindings().iterator();

         while (iter.hasNext())
         {
           Binding binding = (Binding)iter.next();

           if (binding.queue.isClustered() && binding.queue.isRecoverable())
           {
             // Remove any stranded refs corresponding to refs that might have been in the client buffer but not consumed
             binding.queue.removeStrandedReferences(oldSessionID);
           }
         }

         deliveryIdSequence = maxDeliveryId + 1;
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " recoverDeliveries");
      }
   }

   public void addTemporaryDestination(JBossDestination dest) throws JMSException
   {
      try
      {
         if (closed)
         {
            throw new IllegalStateException("Session is closed");
         }
         if (!dest.isTemporary())
         {
            throw new InvalidDestinationException("Destination:" + dest +
               " is not a temporary destination");
         }

         connectionEndpoint.addTemporaryDestination(dest);

         // Register with the destination manager

         ManagedDestination mDest;

         int fullSize = connectionEndpoint.getDefaultTempQueueFullSize();
         int pageSize = connectionEndpoint.getDefaultTempQueuePageSize();
         int downCacheSize = connectionEndpoint.getDefaultTempQueueDownCacheSize();

         //Temporary destinations are clustered if the post office is clustered

         if (dest.isTopic())
         {
            mDest = new ManagedTopic(dest.getName(), fullSize, pageSize, downCacheSize, postOffice.isClustered());
         }
         else
         {
            mDest = new ManagedQueue(dest.getName(), fullSize, pageSize, downCacheSize, postOffice.isClustered());
         }

         mDest.setTemporary(true);

         dm.registerDestination(mDest);

         if (dest.isQueue())
         {
            Queue coreQueue = new MessagingQueue(nodeId, dest.getName(),
                                     idm.getID(), ms, pm, false, -1, null,
                                       fullSize, pageSize, downCacheSize, postOffice.isClustered(),
                                       sp.getRecoverDeliveriesTimeout());


            Condition cond = new JMSCondition(true, dest.getName());

           // make a binding for this temporary queue

            // temporary queues need to bound on ALL nodes of the cluster
            postOffice.addBinding(new Binding(cond, coreQueue, true), postOffice.isClustered());

            coreQueue.activate();
         }
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " addTemporaryDestination");
      }
   }

   public void deleteTemporaryDestination(JBossDestination dest) throws JMSException
   {
      try
      {
         if (closed)
         {
            throw new IllegalStateException("Session is closed");
         }

         if (!dest.isTemporary())
         {
            throw new InvalidDestinationException("Destination:" + dest +
                                                  " is not a temporary destination");
         }

         ManagedDestination mDest = dm.getDestination(dest.getName(), dest.isQueue());

         if (mDest == null)
         {
            throw new InvalidDestinationException("No such destination: " + dest);
         }

         if (dest.isQueue())
         {
           Binding binding = postOffice.getBindingForQueueName(dest.getName());

           if (binding == null)
           {
             throw new IllegalStateException("Cannot find binding for queue " + dest.getName());
           }

           if (binding.queue.getLocalDistributor().getNumberOfReceivers() != 0)
           {
             throw new IllegalStateException("Cannot delete temporary queue if it has consumer(s)");
           }

           // temporary queues must be unbound on ALL nodes of the cluster

            postOffice.removeBinding(dest.getName(), postOffice.isClustered());
         }
         else
         {
            //Topic
            Collection queues = postOffice.getQueuesForCondition(new JMSCondition(false, dest.getName()), true);

            if (!queues.isEmpty())
           {
              throw new IllegalStateException("Cannot delete temporary topic if it has consumer(s)");
           }

            // There is no need to explicitly unbind the subscriptions for the temp topic, this is because we
            // will not get here unless there are no bindings.
            // Note that you cannot create surable subs on a temp topic
         }

         connectionEndpoint.removeTemporaryDestination(dest);

         dm.unregisterDestination(mDest);
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " deleteTemporaryDestination");
      }
   }

   public void unsubscribe(String subscriptionName) throws JMSException
   {
      log.trace(this + " unsubscribing " + subscriptionName);

      try
      {
         if (closed)
         {
            throw new IllegalStateException("Session is closed");
         }
         if (subscriptionName == null)
         {
            throw new InvalidDestinationException("Destination is null");
         }

         String clientID = connectionEndpoint.getClientID();

         if (clientID == null)
         {
            throw new JMSException("null clientID on connection");
         }

         String queueName = MessageQueueNameHelper.createSubscriptionName(clientID, subscriptionName);

         Binding binding = postOffice.getBindingForQueueName(queueName);

         if (binding == null)
         {
            throw new InvalidDestinationException("Cannot find durable subscription with name " +
                                                  subscriptionName + " to unsubscribe");
         }

         Queue sub = binding.queue;

         // Section 6.11. JMS 1.1.
         // "It is erroneous for a client to delete a durable subscription while it has an active
         // TopicSubscriber for it or while a message received by it is part of a current
         // transaction or has not been acknowledged in the session."

         if (sub.getLocalDistributor().getNumberOfReceivers() != 0)
         {
            throw new IllegalStateException("Cannot unsubscribe durable subscription " +
                                            subscriptionName + " since it has active subscribers");
         }

         //Also if it is clustered we must disallow unsubscribing if it has active consumers on other nodes

         if (sub.isClustered() && postOffice.isClustered())
         {
           Replicator rep = (Replicator)postOffice;

           Map map = rep.get(sub.getName());

           if (!map.isEmpty())
           {
             throw new IllegalStateException("Cannot unsubscribe durable subscription " +
                     subscriptionName + " since it has active subscribers on other nodes");
           }
         }

         postOffice.removeBinding(sub.getName(), sub.isClustered() && postOffice.isClustered());

         String counterName = TopicService.SUBSCRIPTION_MESSAGECOUNTER_PREFIX + sub.getName();

         MessageCounter counter = sp.getMessageCounterManager().unregisterMessageCounter(counterName);

         if (counter == null)
         {
            throw new IllegalStateException("Cannot find counter to remove " + counterName);
         }
      }
      catch (Throwable t)
      {
         throw ExceptionUtil.handleJMSInvocation(t, this + " unsubscribe");
      }
   }

   // Public ---------------------------------------------------------------------------------------

   public ServerConnectionEndpoint getConnectionEndpoint()
   {
      return connectionEndpoint;
   }

   public String toString()
   {
      return "SessionEndpoint[" + id + "]";
   }

   public void deliverAnyWaitingDeliveries(String queueName) throws Exception
   {
     //  First deliver any waiting deliveries

     if (trace) { log.trace("Delivering any waiting deliveries: " + queueName); }

     List toAddBack = null;

     while (true)
     {
       DeliveryRecord dr = (DeliveryRecord)toDeliver.poll(0);

       if (dr == null)
       {
         break;
       }

       if (trace) { log.trace("Considering " + dr); }

       if (queueName == null || dr.queueName.equals(queueName))
       {
         //Need to synchronized to prevent the delivery being performed twice
         synchronized (dr)
         {
           performDelivery(dr.del.getReference(), dr.deliveryID, dr.getConsumer());

           dr.waitingForResponse = false;
         }
       }
       else
       {
         if (toAddBack == null)
         {
           toAddBack = new ArrayList();
         }

         toAddBack.add(dr);
       }
     }

     if (toAddBack != null)
     {
       Iterator iter = toAddBack.iterator();

       while (iter.hasNext())
       {
         toDeliver.put(iter.next());
       }
     }

     if (trace) { log.trace("Done delivering"); }
   }

   public boolean collectDeliveries(Map map, boolean firstNode, String queueName) throws Exception
   {
     if (trace) { log.trace("Collecting deliveries"); }

     boolean gotSome = false;

     if (!firstNode && replicating)
     {
       if (trace) { log.trace("Now collecting"); }

       Iterator iter = deliveries.entrySet().iterator();

       while (iter.hasNext())
       {
         Map.Entry entry = (Map.Entry)iter.next();

         Long l = (Long)entry.getKey();

         long deliveryID = l.longValue();

         DeliveryRecord rec = (DeliveryRecord)entry.getValue();

         if (rec.replicating && (queueName == null || rec.queueName.equals(queueName)))
         {
           Map ids = (Map)map.get(rec.queueName);

           if (ids == null)
           {
             ids = new HashMap();

             map.put(rec.queueName, ids);
           }

           ids.put(new Long(rec.del.getReference().getMessage().getMessageID()), id);

           gotSome = true;

           boolean notify = false;

           //Need to synchronize to prevent delivery occurring more than once - e.g.
           //if replicateDeliveryResponseReceived occurs curently with this
           synchronized (rec)
           {
             if (rec.waitingForResponse)
             {
               //Do the delivery now

               performDelivery(rec.del.getReference(), deliveryID, rec.getConsumer());

                rec.waitingForResponse = false;

                notify = true;
             }
           }

           if (notify)
           {
              synchronized (deliveryLock)
              {
                if (waitingToClose)
                {
                  deliveryLock.notifyAll();
                }
              }
           }
         }
       }
     }

     if (trace) { log.trace("Collected " + map.size() + " deliveries"); }

     return gotSome;
   }

   public void replicateDeliveryResponseReceived(long deliveryID) throws Exception
   {
     //We look up the delivery in the list and actually perform the delivery

     if (trace) { log.trace(this + " replicate delivery response received for delivery " + deliveryID); }

     DeliveryRecord rec = (DeliveryRecord)deliveries.get(new Long(deliveryID));

     if (rec == null)
     {
       //This is ok - may happen at failover

        // This can happen when the failover node is being changed
         // E.g. failover node changes, replicates start getting sent to the new failover node,
         // then the new node requests to collect the deliveries from this node, at which point we deliver
         // all waiting deliveries. Then the responses to the original ones come back.
         // So we can ignore them

         return;
     }

     boolean delivered = false;

    while (true)
     {
       DeliveryRecord dr = (DeliveryRecord)toDeliver.peek();

       if (dr == null)
       {
         if (trace) { log.trace("No more deliveries in list"); }

         break;
       }

       if (trace) { log.trace("Peeked delivery record: " + dr.deliveryID); }

       //Needs to be synchronized to prevent delivery occurring twice e.g. if this occurs at same time as collectDeliveries
       synchronized (dr)
       {
         boolean performDelivery = false;

         if (dr.waitingForResponse)
         {
           if (dr == rec)
           {
             if (trace) { log.trace("Found our delivery"); }

             performDelivery = true;
           }
           else
           {
             if (!delivered)
             {
                // Response has come back out of order - this can happen when the failover node is being changed
               // E.g. failover node changes, replicates start getting sent to the new failover node,
               // then the new node requests to collect the deliveries from this node, at which point we deliver
               // all waiting deliveries. Then the responses to the original ones come back.
               // So we can ignore them
               break;
             }
             else
             {
               //We have delivered ours and possibly any non replicated deliveries too

               break;
             }
           }
         }
         else
         {
           //Non replicated delivery

           if (trace) { log.trace("Non replicated delivery"); }

           performDelivery = true;
         }

         if (performDelivery)
         {
           toDeliver.take();

           performDelivery(dr.del.getReference(), dr.deliveryID, dr.getConsumer());

           delivered = true;

            dr.waitingForResponse = false;

            delivered = true;
         }
       }
     }

     if (delivered)
     {
       synchronized (deliveryLock)
       {
         if (waitingToClose)
         {
           deliveryLock.notifyAll();
         }
       }
     }
   }

   // Package protected ----------------------------------------------------------------------------

   void expireDelivery(Delivery del, Queue expiryQueue) throws Throwable
   {
      if (trace) { log.trace(this + " detected expired message " + del.getReference()); }

      if (expiryQueue != null)
      {
         if (trace) { log.trace(this + " sending expired message to expiry queue " + expiryQueue); }

         JBossMessage copy = makeCopyForDLQOrExpiry(true, del);

         moveInTransaction(copy, del, expiryQueue, true);
      }
      else
      {
         log.warn("No expiry queue has been configured so removing expired " +  del.getReference());

         del.acknowledge(null);
      }
   }

   void removeBrowser(String browserId) throws Exception
   {
      synchronized (browsers)
      {
         if (browsers.remove(browserId) == null)
         {
            throw new IllegalStateException("Cannot find browser with id " + browserId + " to remove");
         }
      }
   }

   void removeConsumer(String consumerId) throws Exception
   {
      synchronized (consumers)
      {
         if (consumers.remove(consumerId) == null)
         {
            if (trace) { log.trace("Cannot find consumer with id " + consumerId + " to remove"); }
            //don't throw, as it maybe called twice from client and server's connection failure handler.
            //throw new IllegalStateException("Cannot find consumer with id " + consumerId + " to remove");
         }
      }
   }

   void localClose() throws Throwable
   {
          
      if (closed)
      {
         //don't throw the exception as it maybe called twice
         if (trace) { log.trace("Session is already closed. "); }
         return;
         //throw new IllegalStateException("Session is already closed");
      }

      if (trace) log.trace(this + " close()");

      //We clone to avoid deadlock http://jira.jboss.org/jira/browse/JBMESSAGING-836
      Map consumersClone;
      synchronized (consumers)
      {
         consumersClone = new HashMap(consumers);
      }

      for( Iterator i = consumersClone.values().iterator(); i.hasNext(); )
      {
         ((ServerConsumerEndpoint)i.next()).localClose();
      }

      consumers.clear();


      //We clone to avoid deadlock http://jira.jboss.org/jira/browse/JBMESSAGING-836
      Map browsersClone;
      synchronized (browsers)
      {
         browsersClone = new HashMap(browsers);
      }

      for( Iterator i = browsersClone.values().iterator(); i.hasNext(); )
      {
         ((ServerBrowserEndpoint)i.next()).localClose();
      }

      browsers.clear();


      //Now cancel any remaining deliveries in reverse delivery order
      //Note we don't maintain order using a LinkedHashMap since then we lose
      //concurrency since we would have to lock it exclusively

      synchronized (deliveries)
      {
         List entries = new ArrayList(deliveries.entrySet());

         //Sort them in reverse delivery id order
         Collections.sort(entries,
                       new Comparator()
                       {
                           public int compare(Object obj1, Object obj2)
                           {
                              Map.Entry entry1 = (Map.Entry)obj1;
                              Map.Entry entry2 = (Map.Entry)obj2;
                              Long id1 = (Long)entry1.getKey();
                              Long id2 = (Long)entry2.getKey();
                              return id2.compareTo(id1);
                           }
                       });

         Iterator iter = entries.iterator();

         Set channels = new HashSet();

         if (trace) { log.trace(this + " cancelling " + entries.size() + " deliveries"); }

         while (iter.hasNext())
         {
            Map.Entry entry = (Map.Entry)iter.next();

            if (trace) { log.trace(this + " cancelling delivery with delivery id: " + entry.getKey()); }

            DeliveryRecord rec = (DeliveryRecord)entry.getValue();

            /*
             * https://jira.jboss.org/jira/browse/JBMESSAGING-1440
             */
            if (!rec.del.isXAPrepared())
            {
               rec.del.cancel();
            }

            channels.add(rec.del.getObserver());
         }

         promptDelivery(channels);

         //Close down the executor

         //Note we need to wait for ALL tasks to complete NOT just one otherwise we can end up with the following situation
         //prompter is queued and starts to execute
         //prompter almost finishes executing then a message is cancelled due to this session closing
         //this causes another prompter to be queued
         //shutdownAfterProcessingCurrentTask is then called
         //this means the second prompter never runs and the cancelled message doesn't get redelivered
         executor.shutdownAfterProcessingCurrentlyQueuedTasks();

         deliveries.clear();
      }
     
      sp.removeSession(id);

      Dispatcher.instance.unregisterTarget(id, this);

      closed = true;
   }

   void cancelDelivery(long deliveryId) throws Throwable
   {
      DeliveryRecord rec = null;
      synchronized(deliveries)
      {
         rec = (DeliveryRecord)deliveries.remove(new Long(deliveryId));
      }

      if (rec == null)
      {
         throw new IllegalStateException("Cannot find delivery to cancel " + deliveryId);
      }

      rec.del.cancel();
   }

   /*
    * When a consumer closes there may be deliveries where the replication messages has gone out to the backup
    * but the response hasn't been received yet so the messages hasn't actually been delivered.
    * When closing we must wait for these to be delivered before closing, or the message will be "lost" until
    * the session is closed.
    */
   void waitForDeliveriesFromConsumer(String consumerID) throws Exception
   {
    long toWait = DELIVERY_WAIT_TIMEOUT;

    boolean wait;

     synchronized (deliveryLock)
     {
       do
       {
         wait = false;

         long start = System.currentTimeMillis();

         Iterator iter = deliveries.values().iterator();

         while (iter.hasNext())
         {
           DeliveryRecord rec = (DeliveryRecord)iter.next();

           ServerConsumerEndpoint consumer = rec.getConsumer();

           if (consumer != null && consumer.getID().equals(consumerID) && rec.waitingForResponse)
           {
             wait = true;

             break;
           }
         }

         if (wait)
         {
           waitingToClose = true;
           try
           {
             deliveryLock.wait(toWait);
           }
           catch (InterruptedException e)
           {
             //Ignore
           }
           toWait -= (System.currentTimeMillis() - start);
         }
       }
       while (wait && toWait > 0);

       if (toWait <= 0)
       {
         //Clear toDeliver
         while (toDeliver.poll(0) != null)
         {
         }

         log.warn("Timed out waiting for response to arrive");
       }
       waitingToClose = false;
     }
   }

   //TODO NOTE! This needs to be synchronized to prevent deliveries coming back
   //out of order! There maybe some better way of doing this
   synchronized void handleDelivery(Delivery delivery, ServerConsumerEndpoint consumer) throws Exception
   {
      if (trace) { log.trace(this + " handling delivery " + delivery); }

      DeliveryRecord rec = null;

      long deliveryId = deliveryIdSequence++;

      if (trace) { log.trace("Delivery id is now " + deliveryId); }

      //TODO can't we combine flags isRetainDeliveries and isReplicating - surely they're mutually exclusive?
       if (consumer.isRetainDeliveries())
       {
         // Add a delivery

         rec = new DeliveryRecord(delivery, consumer, deliveryId);

          deliveries.put(new Long(deliveryId), rec);

          if (trace) { log.trace(this + " added delivery " + deliveryId + ": " + delivery); }
       }
       else
       {
         //Acknowledge it now
         try
         {
           //This basically just releases the memory reference

           if (trace) { log.trace("Acknowledging delivery now"); }

           delivery.acknowledge(null);
         }
         catch (Throwable t)
         {
           log.error("Failed to acknowledge delivery", t);
         }
       }

       Message message = delivery.getReference().getMessage();

       //Note that we only replicate transacted or client acknowledge sessions
       //There is no point in replicating AUTO_ACK or DUPS_OK
       if (!consumer.isReplicating() || !replicating)
       {
         if (trace) { log.trace(this + " doing the delivery straight away"); }

         //Actually do the delivery now
         performDelivery(delivery.getReference(), deliveryId, consumer);
       }
       else if (!message.isReliable())
       {
         if (!toDeliver.isEmpty())
         {
           if (trace) { log.trace("Message is unreliable and there are refs in the toDeliver so adding to list"); }

           //We need to add to the list to prevent non persistent messages overtaking persistent messages from the same
           //producer in flight (since np don't need to be replicated)
           toDeliver.put(rec);

           //Race check (there's a small chance the message in the queue got removed between the empty check
           //and the put so we do another check:
           if (toDeliver.peek() == rec)
           {
             toDeliver.take();

             performDelivery(delivery.getReference(), deliveryId, consumer);
           }
         }
         else
         {
           if (trace) { log.trace("Message is unreliable, but no deliveries in list so performing delivery now"); }

           // Actually do the delivery now
            performDelivery(delivery.getReference(), deliveryId, consumer);
         }
       }
       else
       {
         if (!postOffice.isFirstNode())
         {
            //We wait for the replication response to come back before actually performing delivery

            if (trace) { log.trace(this + " deferring delivery until we know it's been replicated"); }

            rec.waitingForResponse = true;

            toDeliver.put(rec);

            postOffice.sendReplicateDeliveryMessage(consumer.getQueueName(), id,
                                                     delivery.getReference().getMessage().getMessageID(),
                                                     deliveryId, true, false);
         }
         else
         {
           //Only node in the cluster so deliver now

           rec.waitingForResponse = false;

           if (trace) { log.trace("First node so actually doing delivery now"); }

           // Actually do the delivery now - we are only node in the cluster
            performDelivery(delivery.getReference(), deliveryId, consumer);
         }
       }
   }

   void performDelivery(MessageReference ref, long deliveryID, ServerConsumerEndpoint consumer)
   {
     if (consumer == null)
     {
       if (trace) { log.trace(this + " consumer is null, cannot perform delivery"); }

       return;
     }

      if (consumer.isDead())
      {
         //Ignore any responses that come back after consumer has died
         return;
      }

     if (trace) { log.trace(this + " performing delivery for " + ref); }

      // We send the message to the client on the current thread. The message is written onto the
      // transport and then the thread returns immediately without waiting for a response.

      ClientDelivery del = new ClientDelivery(ref.getMessage(), consumer.getID(), deliveryID, ref.getDeliveryCount());

      Callback callback = new Callback(del);

      try
      {
         // one way invocation, no acknowledgment sent back by the client
         if (trace) { log.trace(this + " submitting message " + ref.getMessage() + " to the remoting layer to be sent asynchronously"); }
        
         callbackHandler.handleCallbackOneway(callback);

         //We store the delivery id so we know to wait for any deliveries in transit on close
         consumer.setLastDeliveryID(deliveryID);
      }
      catch (Throwable t)
      {
         // it's an oneway callback, so exception could only have happened on the server, while
         // trying to send the callback. This is a good reason to smack the whole connection.
         // I trust remoting to have already done its own cleanup via a CallbackErrorHandler,
         // I need to do my own cleanup at ConnectionManager level.

         log.trace(this + " failed to handle callback", t);

         //We stop the consumer - some time later the lease will expire and the connection will be closed
         //which will remove the consumer

         consumer.setStarted(false);

         consumer.setDead();

         //** IMPORTANT NOTE! We must return the delivery NOT null. **
         //This is because if we return NULL then message will remain in the queue, but later
         //the connection checker will cleanup and close this consumer which will cancel all the deliveries in it
         //including this one, so the message will go back on the queue twice!
      }
   }

   void acknowledgeTransactionally(List acks, Transaction tx) throws Throwable
   {
      if (trace) { log.trace(this + " acknowledging transactionally " + acks.size() + " messages for " + tx); }

      DeliveryCallback deliveryCallback = (DeliveryCallback)tx.getCallback(this);

      if (deliveryCallback == null)
      {
         deliveryCallback = new DeliveryCallback();
         tx.addCallback(deliveryCallback, this);
      }

      for(Iterator i = acks.iterator(); i.hasNext(); )
      {
         Ack ack = (Ack)i.next();

         Long id = new Long(ack.getDeliveryID());

         //TODO - do this more elegantly
         if (ack instanceof DeliveryInfo)
         {
           if (!((DeliveryInfo)ack).isShouldAck())
           {
             //If we are in VM then acks for non durable subs will still exist - this
             //won't happen remoptely since they are not written to the wire
             continue;
           }
         }

         DeliveryRecord rec = (DeliveryRecord)deliveries.get(id);

         if (rec == null)
         {
            log.warn("Cannot find delivery to acknowledge " + ack);
            continue;
         }

         deliveryCallback.addDeliveryId(id);
         rec.del.acknowledge(tx);
      }
   }

   /**
    * Starts this session's Consumers
    */
   void setStarted(boolean s) throws Throwable
   {
      //We clone to prevent deadlock http://jira.jboss.org/jira/browse/JBMESSAGING-836
      Map consumersClone;
      synchronized(consumers)
      {
         consumersClone = new HashMap(consumers);
      }

      for(Iterator i = consumersClone.values().iterator(); i.hasNext(); )
      {
         ServerConsumerEndpoint sce = (ServerConsumerEndpoint)i.next();
         if (s)
         {
            sce.start();
         }
         else
         {
            sce.stop();
         }
      }
   }

   void promptDelivery(final Channel channel)
   {
     if (trace) { log.trace("Prompting delivery on " + channel); }

      try
      {
         //Prompting delivery must be asynchronous to avoid deadlock
         //but we cannot use one way invocations on cancelDelivery and
         //cancelDeliveries because remoting one way invocations can
         //overtake each other in flight - this problem will
         //go away when we have our own transport and our dedicated connection
         this.executor.execute(new Runnable() { public void run() { channel.deliver();} } );

      }
      catch (Throwable t)
      {
         log.error("Failed to prompt delivery", t);
      }
   }

   /** Helper method to valid destinations on the destinationManager */
   void validateDestination(JBossDestination jmsDestination)
           throws InvalidDestinationException
   {
      if (dm.getDestination(jmsDestination.getName(), jmsDestination.isQueue()) == null)
      {
         throw new InvalidDestinationException("No such destination: " + jmsDestination);
      }
   }

   // Protected ------------------------------------------------------------------------------------

   // Private --------------------------------------------------------------------------------------

   private Delivery cancelDeliveryInternal(Cancel cancel) throws Throwable
   {
      DeliveryRecord rec = null;
      synchronized (deliveries)
      {
         rec = (DeliveryRecord)deliveries.remove(new Long(cancel.getDeliveryId()));
      }

      if (rec == null)
      {
         //The delivery might not be found, if the session is not replicated (i.e. auto_ack or dups_ok)
        //and has failed over since recoverDeliveries won't have been called
        if (trace)
        {
          log.trace("Cannot find delivery to cancel, session probably failed over and is not replicated");
        }
        return null;
      }

      //Note we check the flag *and* evaluate again, this is because the server and client clocks may
      //be out of synch and don't want to send back to the client a message it thought it has sent to
      //the expiry queue
      boolean expired = cancel.isExpired() || rec.del.getReference().getMessage().isExpired();

      //Note we check the flag *and* evaluate again, this is because the server value of maxDeliveries
      //might get changed after the client has sent the cancel - and we don't want to end up cancelling
      //back to the original queue
      boolean reachedMaxDeliveryAttempts =
         cancel.isReachedMaxDeliveryAttempts() || cancel.getDeliveryCount() >= rec.maxDeliveryAttempts;

      Delivery del = rec.del;

      if (!expired && !reachedMaxDeliveryAttempts)
      {
         //Normal cancel back to the queue

         del.getReference().setDeliveryCount(cancel.getDeliveryCount());

         //Do we need to set a redelivery delay?

         if (rec.redeliveryDelay != 0)
         {
            del.getReference().setScheduledDeliveryTime(System.currentTimeMillis() + rec.redeliveryDelay);
         }

         if (trace) { log.trace("Cancelling delivery " + cancel.getDeliveryId()); }
         del.cancel();
      }
      else
      {
         if (expired)
         {
            //Sent to expiry queue

            if (debug) { log.debug("Sending expired message: " + rec.del.getReference() + " to expiry queue " + rec.expiryQueue); }
            JBossMessage copy = makeCopyForDLQOrExpiry(true, del);

            moveInTransaction(copy, del, rec.expiryQueue, false);
         }
         else
         {
            //Send to DLQ
            if (debug) { log.debug("Sending message: " + rec.del.getReference() + " to dlq " + rec.dlq); }

            JBossMessage copy = makeCopyForDLQOrExpiry(false, del);

            moveInTransaction(copy, del, rec.dlq, true);
         }
      }

      //Need to send a message to the replicant to remove the id
      postOffice.sendReplicateAckMessage(rec.queueName, del.getReference().getMessage().getMessageID());

      return rec.del;
   }

   private JBossMessage makeCopyForDLQOrExpiry(boolean expiry, Delivery del) throws Exception
   {
      //We copy the message and send that to the dlq/expiry queue - this is because
      //otherwise we may end up with a ref with the same message id in the queue more than once
      //which would barf - this might happen if the same message had been expire from multiple
      //subscriptions of a topic for example
      //We set headers that hold the original message destination, expiry time and original message id

     if (trace) { log.trace("Making copy of message for DLQ or expiry " + del); }

      JBossMessage msg = ((JBossMessage)del.getReference().getMessage());

      JBossMessage copy = msg.doCopy();

      long newMessageId = sp.getMessageIDManager().getID();

      copy.setMessageId(newMessageId);

      //reset expiry
      copy.setExpiration(0);

      String origMessageId = msg.getJMSMessageID();

      String origDest = msg.getJMSDestination().toString();

      copy.setStringProperty(JBossMessage.JBOSS_MESSAGING_ORIG_MESSAGE_ID, origMessageId);

      copy.setStringProperty(JBossMessage.JBOSS_MESSAGING_ORIG_DESTINATION, origDest);

      if (expiry)
      {
         long actualExpiryTime = System.currentTimeMillis();

         copy.setLongProperty(JBossMessage.JBOSS_MESSAGING_ACTUAL_EXPIRY_TIME, actualExpiryTime);
      }

      return copy;
   }

   private void moveInTransaction(JBossMessage msg, Delivery del, Queue queue, boolean dlq) throws Throwable
   {
      Transaction tx = tr.createTransaction();

      MessageReference ref = msg.createReference();

      try
      {
         if (queue != null)
         {
            queue.handle(null, ref, tx);
            del.acknowledge(tx);
         }
         else
         {
            log.warn("No " + (dlq ? "DLQ" : "expiry queue") + " has been specified so the message will be removed");

            del.acknowledge(tx);
         }

         tx.commit();
      }
      catch (Throwable t)
      {
         tx.rollback();
         throw t;
      }

      //Need to prompt delivery on the dlq/expiry queue

      //TODO - are we sure this is the right place to prompt delivery?
      if (queue != null)
      {
         promptDelivery(queue);
      }
   }

   private boolean acknowledgeDeliveryInternal(Ack ack) throws Throwable
   {
      if (trace) { log.trace(this + " acknowledging delivery " + ack); }

      DeliveryRecord rec = null;
     
      //I put synchronized here to prevent the following from happening:
      //a clustered server node detects connection failure and cancel deliveries.
      //but the consumer on it get through to here
      //if not synchronized, the remove may get the record before the above cancel action clear up the deliveries map.
      //so the cancel action makes the message back to queue and this method cause the delivery count to decrement.
      //as the cancel will decrease the delivery count once, so this will result the delivery count being decremented twice
      //for one same message.
      synchronized (deliveries)
      {
         rec = (DeliveryRecord)deliveries.remove(new Long(ack.getDeliveryID()));
      }

      if (rec == null)
      {
        //This can happen if an ack comes in after failover
         log.debug("Cannot find " + ack + " to acknowledge, it was probably acknowledged before");
         return false;
      }

      ServerConsumerEndpoint consumer = rec.getConsumer();

      if (consumer != null && consumer.isRemote())
      {
         //Optimisation for shared DB - we don't ack in DB - we move
         rec.del.getObserver().acknowledgeNoPersist(rec.del);
      }
      else
      {
         rec.del.acknowledge(null);
      }

      //Now replicate the ack

      if (rec.replicating && replicating)
      {
        postOffice.sendReplicateAckMessage(rec.queueName, rec.del.getReference().getMessage().getMessageID());
      }

      if (trace) { log.trace(this + " acknowledged delivery " + ack); }

      return true;
   }

   /* TODO We can combine this with createConsumerDelegateInternal once we move the distinction between queues and topics
    * to the client side, so the server side just deals with queues named by string - i.e MessagingQueue instances
    */
   private ConsumerDelegate createConsumerDelegateDirect(String queueName, String selectorString) throws Throwable
   {
     if (closed)
      {
         throw new IllegalStateException("Session is closed");
      }

      if ("".equals(selectorString))
      {
         selectorString = null;
      }

      if (trace)
      {
         log.trace(this + " creating direct consumer for " + queueName +
            (selectorString == null ? "" : ", selector '" + selectorString + "'"));
      }

      Binding binding =  postOffice.getBindingForQueueName(queueName);

      if (binding == null)
      {
        throw new IllegalArgumentException("Cannot find queue with name " + queueName);
      }

      String consumerID = GUIDGenerator.generateGUID();

      int prefetchSize = connectionEndpoint.getPrefetchSize();

      JBossDestination dest = new JBossQueue(queueName);

      //We don't care about redelivery delays and number of attempts for a direct consumer

      ServerConsumerEndpoint ep =
         new ServerConsumerEndpoint(consumerID, binding.queue,
                                    binding.queue.getName(), this, selectorString, false,
                                    dest, null, null, 0, -1, true, false, prefetchSize);

      ConsumerAdvised advised;

      // Need to synchronized to prevent a deadlock
      // See http://jira.jboss.com/jira/browse/JBMESSAGING-797
      synchronized (AspectManager.instance())
      {
         advised = new ConsumerAdvised(ep);
      }

      Dispatcher.instance.registerTarget(consumerID, advised);

      ClientConsumerDelegate stub =
         new ClientConsumerDelegate(consumerID, prefetchSize, -1, 0);

      synchronized (consumers)
      {
         consumers.put(consumerID, ep);
      }

      log.trace(this + " created and registered " + ep);

      return stub;
   }

   private ConsumerDelegate createConsumerDelegateInternal(JBossDestination jmsDestination,
                                                           String selectorString,
                                                           boolean noLocal,
                                                           String subscriptionName)
      throws Throwable
   {
      if (closed)
      {
         throw new IllegalStateException("Session is closed");
      }

      if ("".equals(selectorString))
      {
         selectorString = null;
      }

      if (trace)
      {
         log.trace(this + " creating consumer for " + jmsDestination +
            (selectorString == null ? "" : ", selector '" + selectorString + "'") +
            (subscriptionName == null ? "" : ", subscription '" + subscriptionName + "'") +
            (noLocal ? ", noLocal" : ""));
      }

      ManagedDestination mDest = dm.getDestination(jmsDestination.getName(), jmsDestination.isQueue());

      if (mDest == null)
      {
         throw new InvalidDestinationException("No such destination: " + jmsDestination + " has it been deployed?");
      }

      if (jmsDestination.isTemporary())
      {
         // Can only create a consumer for a temporary destination on the same connection
         // that created it
         if (!connectionEndpoint.hasTemporaryDestination(jmsDestination))
         {
            String msg = "Cannot create a message consumer on a different connection " +
                         "to that which created the temporary destination";
            throw new IllegalStateException(msg);
         }
      }

      String consumerID = GUIDGenerator.generateGUID();

      // Always validate the selector first
      Selector selector = null;

      if (selectorString != null)
      {
         selector = new Selector(selectorString);
      }

      Queue queue;

      if (jmsDestination.isTopic())
      {
         if (subscriptionName == null)
         {
            // non-durable subscription
            if (log.isTraceEnabled()) { log.trace(this + " creating new non-durable subscription on " + jmsDestination); }

            // Create the non durable sub

            queue = new MessagingQueue(nodeId, GUIDGenerator.generateGUID(),
                                idm.getID(), ms, pm, false,
                                mDest.getMaxSize(), selector,
                                mDest.getFullSize(),
                                mDest.getPageSize(),
                                mDest.getDownCacheSize(),
                                mDest.isClustered(),
                                sp.getRecoverDeliveriesTimeout());

            JMSCondition topicCond = new JMSCondition(false, jmsDestination.getName());

            postOffice.addBinding(new Binding(topicCond, queue, false), false);

            queue.activate();

            String counterName = TopicService.SUBSCRIPTION_MESSAGECOUNTER_PREFIX + queue.getName();

            int dayLimitToUse = mDest.getMessageCounterHistoryDayLimit();
            if (dayLimitToUse == -1)
            {
               //Use override on server peer
               dayLimitToUse = sp.getDefaultMessageCounterHistoryDayLimit();
            }

            //We don't create message counters on temp topics
            if (!mDest.isTemporary())
            {
              MessageCounter counter =  new MessageCounter(counterName, null, queue, true, false, dayLimitToUse);

              sp.getMessageCounterManager().registerMessageCounter(counterName, counter);
            }
         }
         else
         {
            if (jmsDestination.isTemporary())
            {
               throw new InvalidDestinationException("Cannot create a durable subscription on a temporary topic");
            }

            // We have a durable subscription, look it up
            String clientID = connectionEndpoint.getClientID();
            if (clientID == null)
            {
               throw new JMSException("Cannot create durable subscriber without a valid client ID");
            }

            // See if there any bindings with the same client_id.subscription_name name

            String name = MessageQueueNameHelper.createSubscriptionName(clientID, subscriptionName);

            Binding binding = postOffice.getBindingForQueueName(name);

            if (binding == null)
            {
               // Does not already exist

               if (trace) { log.trace(this + " creating new durable subscription on " + jmsDestination); }

               queue = new MessagingQueue(nodeId, name, idm.getID(),
                                          ms, pm, true,
                                          mDest.getMaxSize(), selector,
                                          mDest.getFullSize(),
                                          mDest.getPageSize(),
                                          mDest.getDownCacheSize(),
                                          mDest.isClustered(),
                                          sp.getRecoverDeliveriesTimeout());

               // Durable subs must be bound on ALL nodes of the cluster (if clustered)

               postOffice.addBinding(new Binding(new JMSCondition(false, jmsDestination.getName()), queue, true),
                                     postOffice.isClustered() && mDest.isClustered());

               queue.activate();

               //We don't create message counters on temp topics
               if (!mDest.isTemporary())
               {
                 String counterName = TopicService.SUBSCRIPTION_MESSAGECOUNTER_PREFIX + queue.getName();

                 MessageCounter counter =
                    new MessageCounter(counterName, subscriptionName, queue, true, true,
                                       mDest.getMessageCounterHistoryDayLimit());

                 sp.getMessageCounterManager().registerMessageCounter(counterName, counter);
               }
            }
            else
            {
               //Durable sub already exists

              queue = binding.queue;

               if (trace) { log.trace(this + " subscription " + subscriptionName + " already exists"); }

              //Check if it is already has a subscriber
              //We can't have more than one subscriber at a time on the durable sub unless it is clustered
               //we need this for clustered since otherwise we wouldn't be able to fail over subcribers for the same durable
               //sub onto a node which already has one

               if (queue.getLocalDistributor().getNumberOfReceivers() > 0 && !mDest.isClustered())
               {
                 throw new IllegalStateException("Cannot create a subscriber on the durable subscription since it already has subscriber(s)");
               }

               // If the durable sub exists because it is clustered and was created on this node due to a bind on another node
               // then it will have no message counter

               String counterName = TopicService.SUBSCRIPTION_MESSAGECOUNTER_PREFIX + queue.getName();

               boolean createCounter = false;

               if (sp.getMessageCounterManager().getMessageCounter(counterName) == null)
               {
                 createCounter = true;
               }

               // From javax.jms.Session Javadoc (and also JMS 1.1 6.11.1):
               // A client can change an existing durable subscription by creating a durable
               // TopicSubscriber with the same name and a new topic and/or message selector.
               // Changing a durable subscriber is equivalent to unsubscribing (deleting) the old
               // one and creating a new one.

               String filterString = queue.getFilter() != null ? queue.getFilter().getFilterString() : null;

               boolean selectorChanged =
                  (selectorString == null && filterString != null) ||
                  (filterString == null && selectorString != null) ||
                  (filterString != null && selectorString != null &&
                           !filterString.equals(selectorString));

               if (trace) { log.trace("selector " + (selectorChanged ? "has" : "has NOT") + " changed"); }

               String oldTopicName = ((JMSCondition)binding.condition).getName();

               boolean topicChanged = !oldTopicName.equals(jmsDestination.getName());

               if (log.isTraceEnabled()) { log.trace("topic " + (topicChanged ? "has" : "has NOT") + " changed"); }

               if (selectorChanged || topicChanged)
               {
                  if (trace) { log.trace("topic or selector changed so deleting old subscription"); }

                  // Unbind the durable subscription

                  // Durable subs must be unbound on ALL nodes of the cluster

                  postOffice.removeBinding(queue.getName(), postOffice.isClustered() && mDest.isClustered());

                  // create a fresh new subscription

                  queue = new MessagingQueue(nodeId, name, idm.getID(), ms, pm, true,
                                        mDest.getMaxSize(), selector,
                                        mDest.getFullSize(),
                                        mDest.getPageSize(),
                                        mDest.getDownCacheSize(),
                                        mDest.isClustered(),
                                        sp.getRecoverDeliveriesTimeout());

                  // Durable subs must be bound on ALL nodes of the cluster

                  postOffice.addBinding(new Binding(new JMSCondition(false, jmsDestination.getName()), queue, true),
                                      postOffice.isClustered() && mDest.isClustered());

                  queue.activate();

                  if (!mDest.isTemporary())
                  {
                    createCounter = true;
                  }
               }

               if (createCounter)
               {
                 MessageCounter counter =
                     new MessageCounter(counterName, subscriptionName, queue, true, true,
                                        mDest.getMessageCounterHistoryDayLimit());

                  sp.getMessageCounterManager().registerMessageCounter(counterName, counter);
               }
            }
         }
      }
      else
      {
         // Consumer on a jms queue

         // Let's find the queue

        queue = postOffice.getBindingForQueueName(jmsDestination.getName()).queue;

         if (queue == null)
         {
            throw new IllegalStateException("Cannot find queue: " + jmsDestination.getName());
         }
      }

      int prefetchSize = connectionEndpoint.getPrefetchSize();

      Queue dlqToUse = mDest.getDLQ() == null ? defaultDLQ : mDest.getDLQ();

      Queue expiryQueueToUse = mDest.getExpiryQueue() == null ? defaultExpiryQueue : mDest.getExpiryQueue();

      int maxDeliveryAttemptsToUse = mDest.getMaxDeliveryAttempts() == -1 ? defaultMaxDeliveryAttempts : mDest.getMaxDeliveryAttempts();

      long redeliveryDelayToUse = mDest.getRedeliveryDelay() == -1 ? defaultRedeliveryDelay : mDest.getRedeliveryDelay();

      //Is the consumer going to have its session state replicated onto a backup node?

      boolean replicating = supportsFailover && queue.isClustered() && !(jmsDestination.isTopic() && !queue.isRecoverable());

      ServerConsumerEndpoint ep =
         new ServerConsumerEndpoint(consumerID, queue,
                                    queue.getName(), this, selectorString, noLocal,
                                    jmsDestination, dlqToUse, expiryQueueToUse, redeliveryDelayToUse,
                                    maxDeliveryAttemptsToUse, false, replicating, prefetchSize);

      if (queue.isClustered() && postOffice.isClustered() && jmsDestination.isTopic() && subscriptionName != null)
      {
        //Clustered durable sub consumer created - we need to add this info in the replicator - it is needed by other nodes

        //This is also used to prevent a possible race condition where a clustered durable sub is bound on all nodes
        //but then unsubscribed before the bind is complete on all nodes, leaving it bound on some nodes and not on others
        //The bind all is synchronous so by the time we add the x to the replicator we know it is bound on all nodes
        //and same to unsubscribe

        Replicator rep = (Replicator)postOffice;

        rep.put(queue.getName(), DUR_SUB_STATE_CONSUMERS);
      }

      ConsumerAdvised advised;

      // Need to synchronized to prevent a deadlock
      // See http://jira.jboss.com/jira/browse/JBMESSAGING-797
      synchronized (AspectManager.instance())
      {
         advised = new ConsumerAdvised(ep);
      }

      Dispatcher.instance.registerTarget(consumerID, advised);

      ClientConsumerDelegate stub =
         new ClientConsumerDelegate(consumerID, prefetchSize, maxDeliveryAttemptsToUse, redeliveryDelayToUse);

      synchronized (consumers)
      {
         consumers.put(consumerID, ep);
      }

      log.trace(this + " created and registered " + ep);

      return stub;
   }

   private BrowserDelegate createBrowserDelegateInternal(JBossDestination jmsDestination,
                                                         String selector) throws Throwable
   {
      if (closed)
      {
         throw new IllegalStateException("Session is closed");
      }

      if (jmsDestination == null)
      {
         throw new InvalidDestinationException("null destination");
      }

      if (jmsDestination.isTopic())
      {
         throw new IllegalStateException("Cannot browse a topic");
      }

      validateDestination(jmsDestination);

      log.trace(this + " creating browser for " + jmsDestination +
         (selector == null ? "" : ", selector '" + selector + "'"));

      Binding binding = postOffice.getBindingForQueueName(jmsDestination.getName());

      if (binding == null)
      {
        throw new IllegalStateException("Cannot find queue with name " + jmsDestination.getName());
      }

      String browserID = GUIDGenerator.generateGUID();

      ServerBrowserEndpoint ep = new ServerBrowserEndpoint(this, browserID, binding.queue, selector, jmsDestination);

      // still need to synchronized since close() can come in on a different thread
      synchronized (browsers)
      {
         browsers.put(browserID, ep);
      }

      BrowserAdvised advised;

      // Need to synchronized to prevent a deadlock
      // See http://jira.jboss.com/jira/browse/JBMESSAGING-797
      synchronized (AspectManager.instance())
      {
         advised = new BrowserAdvised(ep);
      }

      Dispatcher.instance.registerTarget(browserID, advised);

      ClientBrowserDelegate stub = new ClientBrowserDelegate(browserID);

      log.trace(this + " created and registered " + ep);

      return stub;
   }

   private void promptDelivery(Set channels)
   {
      //Now prompt delivery on the channels
      Iterator iter = channels.iterator();

      while (iter.hasNext())
      {
         DeliveryObserver observer = (DeliveryObserver)iter.next();

         promptDelivery((Channel)observer);
      }
   }

   // Inner classes --------------------------------------------------------------------------------

   /*
    * Holds a record of a delivery - we need to store the consumer id as well
    * hence this class
    * We can't rely on the cancel being driven from the ClientConsumer since
    * the deliveries may have got lost in transit (ignored) since the consumer might have closed
    * when they were in transit.
    * In such a case we might otherwise end up with the consumer closing but not all it's deliveries being
    * cancelled, which would mean they wouldn't be cancelled until the session is closed which is too late
    *
    * We need to store various pieces of information, such as consumer id, dlq, expiry queue
    * since we need this at cancel time, but by then the actual consumer might have closed
    */
   private static class DeliveryRecord
   {
     // We need to cache the attributes here  since the consumer may get gc'd BEFORE the delivery is acked

      Delivery del;

      Queue dlq;

      Queue expiryQueue;

      long redeliveryDelay;

      int maxDeliveryAttempts;

      WeakReference consumerRef;

      String queueName;

      boolean replicating;

      volatile boolean waitingForResponse;

      long deliveryID;

      ServerConsumerEndpoint getConsumer()
      {
        if (consumerRef != null)
        {
          return (ServerConsumerEndpoint)consumerRef.get();
        }
        else
        {
          return null;
        }
      }

      DeliveryRecord(Delivery del, Queue dlq, Queue expiryQueue, long redeliveryDelay, int maxDeliveryAttempts,
                   String queueName, boolean replicating, long deliveryID)
      {
        this.del = del;

        this.dlq = dlq;

        this.expiryQueue = expiryQueue;

        this.redeliveryDelay = redeliveryDelay;

        this.maxDeliveryAttempts = maxDeliveryAttempts;

        this.queueName = queueName;

        this.replicating = replicating;

        this.deliveryID = deliveryID;
      }

      DeliveryRecord(Delivery del, ServerConsumerEndpoint consumer, long deliveryID)
      {
        this (del, consumer.getDLQ(), consumer.getExpiryQueue(), consumer.getRedliveryDelay(), consumer.getMaxDeliveryAttempts(),
            consumer.getQueueName(), consumer.isReplicating(), deliveryID);

        // We need to cache the attributes here  since the consumer may get gc'd BEFORE the delivery is acked


         //We hold a WeakReference to the consumer - this is only needed when replicating - where we store the delivery then wait
         //for the response to come back from the replicant before actually performing delivery
         //We need a weak ref since when the consumer closes deliveries may still and remain and we don't want that to prevent
         //the consumer being gc'd
         this.consumerRef = new WeakReference(consumer);
      }

     public String toString()
     {
       return "DeliveryRecord " + System.identityHashCode(this) + " del: " + del + " queueName: " + queueName;
     }
   }

   /**
    *
    * The purpose of this class is to remove deliveries from the delivery list on commit
    * Each transaction has once instance of this per SCE
    *
    */
   private class DeliveryCallback implements TxCallback
   {
      List delList = new ArrayList();

      public void beforePrepare()
      {
         //NOOP
      }

      public void beforeCommit(boolean onePhase)
      {
         //NOOP
      }

      public void beforeRollback(boolean onePhase)
      {
         //NOOP
      }

      public void afterPrepare()
      {
         //NOOP
      }

      public synchronized void afterCommit(boolean onePhase) throws TransactionException
      {
         // Remove the deliveries from the delivery map.
         Iterator iter = delList.iterator();
         while (iter.hasNext())
         {
            Long deliveryId = (Long)iter.next();

            DeliveryRecord del = (DeliveryRecord)deliveries.remove(deliveryId);

            if (del != null && del.replicating)
            {
              //TODO - we could batch this in one message
              try
              {
                postOffice.sendReplicateAckMessage(del.queueName, del.del.getReference().getMessage().getMessageID());
              }
              catch (Exception e)
              {
                throw new TransactionException("Failed to handle send ack", e);
              }
            }
         }
      }

      public void afterRollback(boolean onePhase) throws TransactionException
      {
         //One phase rollbacks never hit the server - they are dealt with locally only
         //so this would only ever be executed for a two phase rollback.

         //We don't do anything since cancellation is driven from the client.
      }

      synchronized void addDeliveryId(Long deliveryId)
      {
         delList.add(deliveryId);
      }
   }

}
TOP

Related Classes of org.jboss.jms.server.endpoint.ServerSessionEndpoint

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.