001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.broker.region;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.Iterator;
022import java.util.LinkedList;
023import java.util.List;
024import java.util.concurrent.CountDownLatch;
025import java.util.concurrent.TimeUnit;
026
027import javax.jms.JMSException;
028
029import org.apache.activemq.broker.Broker;
030import org.apache.activemq.broker.ConnectionContext;
031import org.apache.activemq.broker.region.cursors.PendingMessageCursor;
032import org.apache.activemq.broker.region.cursors.VMPendingMessageCursor;
033import org.apache.activemq.command.ConsumerControl;
034import org.apache.activemq.command.ConsumerInfo;
035import org.apache.activemq.command.Message;
036import org.apache.activemq.command.MessageAck;
037import org.apache.activemq.command.MessageDispatch;
038import org.apache.activemq.command.MessageDispatchNotification;
039import org.apache.activemq.command.MessageId;
040import org.apache.activemq.command.MessagePull;
041import org.apache.activemq.command.Response;
042import org.apache.activemq.thread.Scheduler;
043import org.apache.activemq.transaction.Synchronization;
044import org.apache.activemq.transport.TransmitCallback;
045import org.apache.activemq.usage.SystemUsage;
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048
049/**
050 * A subscription that honors the pre-fetch option of the ConsumerInfo.
051 */
052public abstract class PrefetchSubscription extends AbstractSubscription {
053
054    private static final Logger LOG = LoggerFactory.getLogger(PrefetchSubscription.class);
055    protected final Scheduler scheduler;
056
057    protected PendingMessageCursor pending;
058    protected final List<MessageReference> dispatched = new ArrayList<MessageReference>();
059    private int maxProducersToAudit=32;
060    private int maxAuditDepth=2048;
061    protected final SystemUsage usageManager;
062    protected final Object pendingLock = new Object();
063    protected final Object dispatchLock = new Object();
064    private final CountDownLatch okForAckAsDispatchDone = new CountDownLatch(1);
065
066    public PrefetchSubscription(Broker broker, SystemUsage usageManager, ConnectionContext context, ConsumerInfo info, PendingMessageCursor cursor) throws JMSException {
067        super(broker,context, info);
068        this.usageManager=usageManager;
069        pending = cursor;
070        try {
071            pending.start();
072        } catch (Exception e) {
073            throw new JMSException(e.getMessage());
074        }
075        this.scheduler = broker.getScheduler();
076    }
077
078    public PrefetchSubscription(Broker broker,SystemUsage usageManager, ConnectionContext context, ConsumerInfo info) throws JMSException {
079        this(broker,usageManager,context, info, new VMPendingMessageCursor(false));
080    }
081
082    /**
083     * Allows a message to be pulled on demand by a client
084     */
085    @Override
086    public Response pullMessage(ConnectionContext context, final MessagePull pull) throws Exception {
087        // The slave should not deliver pull messages.
088        // TODO: when the slave becomes a master, He should send a NULL message to all the
089        // consumers to 'wake them up' in case they were waiting for a message.
090        if (getPrefetchSize() == 0) {
091            prefetchExtension.set(pull.getQuantity());
092            final long dispatchCounterBeforePull = getSubscriptionStatistics().getDispatched().getCount();
093
094            // Have the destination push us some messages.
095            for (Destination dest : destinations) {
096                dest.iterate();
097            }
098            dispatchPending();
099
100            synchronized(this) {
101                // If there was nothing dispatched.. we may need to setup a timeout.
102                if (dispatchCounterBeforePull == getSubscriptionStatistics().getDispatched().getCount() || pull.isAlwaysSignalDone()) {
103                    // immediate timeout used by receiveNoWait()
104                    if (pull.getTimeout() == -1) {
105                        // Null message indicates the pull is done or did not have pending.
106                        prefetchExtension.set(1);
107                        add(QueueMessageReference.NULL_MESSAGE);
108                        dispatchPending();
109                    }
110                    if (pull.getTimeout() > 0) {
111                        scheduler.executeAfterDelay(new Runnable() {
112                            @Override
113                            public void run() {
114                                pullTimeout(dispatchCounterBeforePull, pull.isAlwaysSignalDone());
115                            }
116                        }, pull.getTimeout());
117                    }
118                }
119            }
120        }
121        return null;
122    }
123
124    /**
125     * Occurs when a pull times out. If nothing has been dispatched since the
126     * timeout was setup, then send the NULL message.
127     */
128    final void pullTimeout(long dispatchCounterBeforePull, boolean alwaysSignalDone) {
129        synchronized (pendingLock) {
130            if (dispatchCounterBeforePull == getSubscriptionStatistics().getDispatched().getCount() || alwaysSignalDone) {
131                try {
132                    prefetchExtension.set(1);
133                    add(QueueMessageReference.NULL_MESSAGE);
134                    dispatchPending();
135                } catch (Exception e) {
136                    context.getConnection().serviceException(e);
137                } finally {
138                    prefetchExtension.set(0);
139                }
140            }
141        }
142    }
143
144    @Override
145    public void add(MessageReference node) throws Exception {
146        synchronized (pendingLock) {
147            // The destination may have just been removed...
148            if (!destinations.contains(node.getRegionDestination()) && node != QueueMessageReference.NULL_MESSAGE) {
149                // perhaps we should inform the caller that we are no longer valid to dispatch to?
150                return;
151            }
152
153            // Don't increment for the pullTimeout control message.
154            if (!node.equals(QueueMessageReference.NULL_MESSAGE)) {
155                getSubscriptionStatistics().getEnqueues().increment();
156            }
157            pending.addMessageLast(node);
158        }
159        dispatchPending();
160    }
161
162    @Override
163    public void processMessageDispatchNotification(MessageDispatchNotification mdn) throws Exception {
164        synchronized(pendingLock) {
165            try {
166                pending.reset();
167                while (pending.hasNext()) {
168                    MessageReference node = pending.next();
169                    node.decrementReferenceCount();
170                    if (node.getMessageId().equals(mdn.getMessageId())) {
171                        // Synchronize between dispatched list and removal of messages from pending list
172                        // related to remove subscription action
173                        synchronized(dispatchLock) {
174                            pending.remove();
175                            createMessageDispatch(node, node.getMessage());
176                            dispatched.add(node);
177                            getSubscriptionStatistics().getInflightMessageSize().addSize(node.getSize());
178                            onDispatch(node, node.getMessage());
179                        }
180                        return;
181                    }
182                }
183            } finally {
184                pending.release();
185            }
186        }
187        throw new JMSException(
188                "Slave broker out of sync with master: Dispatched message ("
189                        + mdn.getMessageId() + ") was not in the pending list for "
190                        + mdn.getConsumerId() + " on " + mdn.getDestination().getPhysicalName());
191    }
192
193    @Override
194    public final void acknowledge(final ConnectionContext context,final MessageAck ack) throws Exception {
195        // Handle the standard acknowledgment case.
196        boolean callDispatchMatched = false;
197        Destination destination = null;
198
199        if (!okForAckAsDispatchDone.await(0l, TimeUnit.MILLISECONDS)) {
200            // suppress unexpected ack exception in this expected case
201            LOG.warn("Ignoring ack received before dispatch; result of failover with an outstanding ack. Acked messages will be replayed if present on this broker. Ignored ack: {}", ack);
202            return;
203        }
204
205        LOG.trace("ack: {}", ack);
206
207        synchronized(dispatchLock) {
208            if (ack.isStandardAck()) {
209                // First check if the ack matches the dispatched. When using failover this might
210                // not be the case. We don't ever want to ack the wrong messages.
211                assertAckMatchesDispatched(ack);
212
213                // Acknowledge all dispatched messages up till the message id of
214                // the acknowledgment.
215                boolean inAckRange = false;
216                List<MessageReference> removeList = new ArrayList<MessageReference>();
217                for (final MessageReference node : dispatched) {
218                    MessageId messageId = node.getMessageId();
219                    if (ack.getFirstMessageId() == null
220                            || ack.getFirstMessageId().equals(messageId)) {
221                        inAckRange = true;
222                    }
223                    if (inAckRange) {
224                        // Don't remove the nodes until we are committed.
225                        if (!context.isInTransaction()) {
226                            getSubscriptionStatistics().getDequeues().increment();
227                            ((Destination)node.getRegionDestination()).getDestinationStatistics().getInflight().decrement();
228                            removeList.add(node);
229                        } else {
230                            registerRemoveSync(context, node);
231                        }
232                        acknowledge(context, ack, node);
233                        if (ack.getLastMessageId().equals(messageId)) {
234                            destination = (Destination) node.getRegionDestination();
235                            callDispatchMatched = true;
236                            break;
237                        }
238                    }
239                }
240                for (final MessageReference node : removeList) {
241                    dispatched.remove(node);
242                    getSubscriptionStatistics().getInflightMessageSize().addSize(-node.getSize());
243                }
244                // this only happens after a reconnect - get an ack which is not
245                // valid
246                if (!callDispatchMatched) {
247                    LOG.warn("Could not correlate acknowledgment with dispatched message: {}", ack);
248                }
249            } else if (ack.isIndividualAck()) {
250                // Message was delivered and acknowledge - but only delete the
251                // individual message
252                for (final MessageReference node : dispatched) {
253                    MessageId messageId = node.getMessageId();
254                    if (ack.getLastMessageId().equals(messageId)) {
255                        // Don't remove the nodes until we are committed - immediateAck option
256                        if (!context.isInTransaction()) {
257                            getSubscriptionStatistics().getDequeues().increment();
258                            ((Destination)node.getRegionDestination()).getDestinationStatistics().getInflight().decrement();
259                            dispatched.remove(node);
260                            getSubscriptionStatistics().getInflightMessageSize().addSize(-node.getSize());
261                        } else {
262                            registerRemoveSync(context, node);
263                        }
264
265                        if (isUsePrefetchExtension() && getPrefetchSize() != 0 && ack.isInTransaction()) {
266                            // allow transaction batch to exceed prefetch
267                            while (true) {
268                                int currentExtension = prefetchExtension.get();
269                                int newExtension = Math.max(currentExtension, currentExtension + 1);
270                                if (prefetchExtension.compareAndSet(currentExtension, newExtension)) {
271                                    break;
272                                }
273                            }
274                        }
275
276                        acknowledge(context, ack, node);
277                        destination = (Destination) node.getRegionDestination();
278                        callDispatchMatched = true;
279                        break;
280                    }
281                }
282            }else if (ack.isDeliveredAck()) {
283                // Message was delivered but not acknowledged: update pre-fetch
284                // counters.
285                int index = 0;
286                for (Iterator<MessageReference> iter = dispatched.iterator(); iter.hasNext(); index++) {
287                    final MessageReference node = iter.next();
288                    Destination nodeDest = (Destination) node.getRegionDestination();
289                    if (ack.getLastMessageId().equals(node.getMessageId())) {
290                        if (isUsePrefetchExtension() && getPrefetchSize() != 0) {
291                            // allow  batch to exceed prefetch
292                            while (true) {
293                                int currentExtension = prefetchExtension.get();
294                                int newExtension = Math.max(currentExtension, index + 1);
295                                if (prefetchExtension.compareAndSet(currentExtension, newExtension)) {
296                                    break;
297                                }
298                            }
299                        }
300                        destination = nodeDest;
301                        callDispatchMatched = true;
302                        break;
303                    }
304                }
305                if (!callDispatchMatched) {
306                    throw new JMSException(
307                            "Could not correlate acknowledgment with dispatched message: "
308                                    + ack);
309                }
310            } else if (ack.isExpiredAck()) {
311                // Message was expired
312                int index = 0;
313                boolean inAckRange = false;
314                for (Iterator<MessageReference> iter = dispatched.iterator(); iter.hasNext(); index++) {
315                    final MessageReference node = iter.next();
316                    Destination nodeDest = (Destination) node.getRegionDestination();
317                    MessageId messageId = node.getMessageId();
318                    if (ack.getFirstMessageId() == null || ack.getFirstMessageId().equals(messageId)) {
319                        inAckRange = true;
320                    }
321                    if (inAckRange) {
322                        Destination regionDestination = nodeDest;
323                        if (broker.isExpired(node)) {
324                            regionDestination.messageExpired(context, this, node);
325                        }
326                        iter.remove();
327                        nodeDest.getDestinationStatistics().getInflight().decrement();
328
329                        if (ack.getLastMessageId().equals(messageId)) {
330                            if (isUsePrefetchExtension() && getPrefetchSize() != 0) {
331                                // allow  batch to exceed prefetch
332                                while (true) {
333                                    int currentExtension = prefetchExtension.get();
334                                    int newExtension = Math.max(currentExtension, index + 1);
335                                    if (prefetchExtension.compareAndSet(currentExtension, newExtension)) {
336                                        break;
337                                    }
338                                }
339                            }
340
341                            destination = (Destination) node.getRegionDestination();
342                            callDispatchMatched = true;
343                            break;
344                        }
345                    }
346                }
347                if (!callDispatchMatched) {
348                    throw new JMSException(
349                            "Could not correlate expiration acknowledgment with dispatched message: "
350                                    + ack);
351                }
352            } else if (ack.isRedeliveredAck()) {
353                // Message was re-delivered but it was not yet considered to be
354                // a DLQ message.
355                boolean inAckRange = false;
356                for (final MessageReference node : dispatched) {
357                    MessageId messageId = node.getMessageId();
358                    if (ack.getFirstMessageId() == null
359                            || ack.getFirstMessageId().equals(messageId)) {
360                        inAckRange = true;
361                    }
362                    if (inAckRange) {
363                        if (ack.getLastMessageId().equals(messageId)) {
364                            destination = (Destination) node.getRegionDestination();
365                            callDispatchMatched = true;
366                            break;
367                        }
368                    }
369                }
370                if (!callDispatchMatched) {
371                    throw new JMSException(
372                            "Could not correlate acknowledgment with dispatched message: "
373                                    + ack);
374                }
375            } else if (ack.isPoisonAck()) {
376                // TODO: what if the message is already in a DLQ???
377                // Handle the poison ACK case: we need to send the message to a
378                // DLQ
379                if (ack.isInTransaction()) {
380                    throw new JMSException("Poison ack cannot be transacted: "
381                            + ack);
382                }
383                int index = 0;
384                boolean inAckRange = false;
385                List<MessageReference> removeList = new ArrayList<MessageReference>();
386                for (final MessageReference node : dispatched) {
387                    MessageId messageId = node.getMessageId();
388                    if (ack.getFirstMessageId() == null
389                            || ack.getFirstMessageId().equals(messageId)) {
390                        inAckRange = true;
391                    }
392                    if (inAckRange) {
393                        sendToDLQ(context, node, ack.getPoisonCause());
394                        Destination nodeDest = (Destination) node.getRegionDestination();
395                        nodeDest.getDestinationStatistics()
396                        .getInflight().decrement();
397                        removeList.add(node);
398                        getSubscriptionStatistics().getDequeues().increment();
399                        index++;
400                        acknowledge(context, ack, node);
401                        if (ack.getLastMessageId().equals(messageId)) {
402                            while (true) {
403                                int currentExtension = prefetchExtension.get();
404                                int newExtension = Math.max(0, currentExtension - (index + 1));
405                                if (prefetchExtension.compareAndSet(currentExtension, newExtension)) {
406                                    break;
407                                }
408                            }
409                            destination = nodeDest;
410                            callDispatchMatched = true;
411                            break;
412                        }
413                    }
414                }
415                for (final MessageReference node : removeList) {
416                    dispatched.remove(node);
417                    getSubscriptionStatistics().getInflightMessageSize().addSize(-node.getSize());
418                }
419                if (!callDispatchMatched) {
420                    throw new JMSException(
421                            "Could not correlate acknowledgment with dispatched message: "
422                                    + ack);
423                }
424            }
425        }
426        if (callDispatchMatched && destination != null) {
427            destination.wakeup();
428            dispatchPending();
429
430            if (pending.isEmpty()) {
431                wakeupDestinationsForDispatch();
432            }
433        } else {
434            LOG.debug("Acknowledgment out of sync (Normally occurs when failover connection reconnects): {}", ack);
435        }
436    }
437
438    private void registerRemoveSync(ConnectionContext context, final MessageReference node) {
439        // setup a Synchronization to remove nodes from the
440        // dispatched list.
441        context.getTransaction().addSynchronization(
442                new Synchronization() {
443
444                    @Override
445                    public void beforeEnd() {
446                        if (isUsePrefetchExtension() && getPrefetchSize() != 0) {
447                            while (true) {
448                                int currentExtension = prefetchExtension.get();
449                                int newExtension = Math.max(0, currentExtension - 1);
450                                if (prefetchExtension.compareAndSet(currentExtension, newExtension)) {
451                                    break;
452                                }
453                            }
454                        }
455                    }
456
457                    @Override
458                    public void afterCommit()
459                            throws Exception {
460                        Destination nodeDest = (Destination) node.getRegionDestination();
461                        synchronized(dispatchLock) {
462                            getSubscriptionStatistics().getDequeues().increment();
463                            dispatched.remove(node);
464                            getSubscriptionStatistics().getInflightMessageSize().addSize(-node.getSize());
465                            nodeDest.getDestinationStatistics().getInflight().decrement();
466                        }
467                        nodeDest.wakeup();
468                        dispatchPending();
469                    }
470
471                    @Override
472                    public void afterRollback() throws Exception {
473                        synchronized(dispatchLock) {
474                            // poisionAck will decrement - otherwise still inflight on client
475                        }
476                    }
477                });
478    }
479
480    /**
481     * Checks an ack versus the contents of the dispatched list.
482     *  called with dispatchLock held
483     * @param ack
484     * @throws JMSException if it does not match
485     */
486    protected void assertAckMatchesDispatched(MessageAck ack) throws JMSException {
487        MessageId firstAckedMsg = ack.getFirstMessageId();
488        MessageId lastAckedMsg = ack.getLastMessageId();
489        int checkCount = 0;
490        boolean checkFoundStart = false;
491        boolean checkFoundEnd = false;
492        for (MessageReference node : dispatched) {
493
494            if (firstAckedMsg == null) {
495                checkFoundStart = true;
496            } else if (!checkFoundStart && firstAckedMsg.equals(node.getMessageId())) {
497                checkFoundStart = true;
498            }
499
500            if (checkFoundStart) {
501                checkCount++;
502            }
503
504            if (lastAckedMsg != null && lastAckedMsg.equals(node.getMessageId())) {
505                checkFoundEnd = true;
506                break;
507            }
508        }
509        if (!checkFoundStart && firstAckedMsg != null)
510            throw new JMSException("Unmatched acknowledge: " + ack
511                    + "; Could not find Message-ID " + firstAckedMsg
512                    + " in dispatched-list (start of ack)");
513        if (!checkFoundEnd && lastAckedMsg != null)
514            throw new JMSException("Unmatched acknowledge: " + ack
515                    + "; Could not find Message-ID " + lastAckedMsg
516                    + " in dispatched-list (end of ack)");
517        if (ack.getMessageCount() != checkCount && !ack.isInTransaction()) {
518            throw new JMSException("Unmatched acknowledge: " + ack
519                    + "; Expected message count (" + ack.getMessageCount()
520                    + ") differs from count in dispatched-list (" + checkCount
521                    + ")");
522        }
523    }
524
525    /**
526     *
527     * @param context
528     * @param node
529     * @param poisonCause
530     * @throws IOException
531     * @throws Exception
532     */
533    protected void sendToDLQ(final ConnectionContext context, final MessageReference node, Throwable poisonCause) throws IOException, Exception {
534        broker.getRoot().sendToDeadLetterQueue(context, node, this, poisonCause);
535    }
536
537    @Override
538    public int getInFlightSize() {
539        return dispatched.size();
540    }
541
542    /**
543     * Used to determine if the broker can dispatch to the consumer.
544     *
545     * @return true if the subscription is full
546     */
547    @Override
548    public boolean isFull() {
549        return getPrefetchSize() == 0 ? prefetchExtension.get() == 0 : dispatched.size() - prefetchExtension.get() >= info.getPrefetchSize();
550    }
551
552    /**
553     * @return true when 60% or more room is left for dispatching messages
554     */
555    @Override
556    public boolean isLowWaterMark() {
557        return (dispatched.size() - prefetchExtension.get()) <= (info.getPrefetchSize() * .4);
558    }
559
560    /**
561     * @return true when 10% or less room is left for dispatching messages
562     */
563    @Override
564    public boolean isHighWaterMark() {
565        return (dispatched.size() - prefetchExtension.get()) >= (info.getPrefetchSize() * .9);
566    }
567
568    @Override
569    public int countBeforeFull() {
570        return getPrefetchSize() == 0 ? prefetchExtension.get() : info.getPrefetchSize() + prefetchExtension.get() - dispatched.size();
571    }
572
573    @Override
574    public int getPendingQueueSize() {
575        return pending.size();
576    }
577
578    @Override
579    public long getPendingMessageSize() {
580        synchronized (pendingLock) {
581            return pending.messageSize();
582        }
583    }
584
585    @Override
586    public int getDispatchedQueueSize() {
587        return dispatched.size();
588    }
589
590    @Override
591    public long getDequeueCounter() {
592        return getSubscriptionStatistics().getDequeues().getCount();
593    }
594
595    @Override
596    public long getDispatchedCounter() {
597        return getSubscriptionStatistics().getDispatched().getCount();
598    }
599
600    @Override
601    public long getEnqueueCounter() {
602        return getSubscriptionStatistics().getEnqueues().getCount();
603    }
604
605    @Override
606    public boolean isRecoveryRequired() {
607        return pending.isRecoveryRequired();
608    }
609
610    public PendingMessageCursor getPending() {
611        return this.pending;
612    }
613
614    public void setPending(PendingMessageCursor pending) {
615        this.pending = pending;
616        if (this.pending!=null) {
617            this.pending.setSystemUsage(usageManager);
618            this.pending.setMemoryUsageHighWaterMark(getCursorMemoryHighWaterMark());
619        }
620    }
621
622    @Override
623    public void add(ConnectionContext context, Destination destination) throws Exception {
624        synchronized(pendingLock) {
625            super.add(context, destination);
626            pending.add(context, destination);
627        }
628    }
629
630    @Override
631    public List<MessageReference> remove(ConnectionContext context, Destination destination) throws Exception {
632        return remove(context, destination, dispatched);
633    }
634
635    public List<MessageReference> remove(ConnectionContext context, Destination destination, List<MessageReference> dispatched) throws Exception {
636        LinkedList<MessageReference> redispatch = new LinkedList<MessageReference>();
637        synchronized(pendingLock) {
638            super.remove(context, destination);
639            // Here is a potential problem concerning Inflight stat:
640            // Messages not already committed or rolled back may not be removed from dispatched list at the moment
641            // Except if each commit or rollback callback action comes before remove of subscriber.
642            redispatch.addAll(pending.remove(context, destination));
643
644            if (dispatched == null) {
645                return redispatch;
646            }
647
648            // Synchronized to DispatchLock if necessary
649            if (dispatched == this.dispatched) {
650                synchronized(dispatchLock) {
651                    addReferencesAndUpdateRedispatch(redispatch, destination, dispatched);
652                }
653            } else {
654                addReferencesAndUpdateRedispatch(redispatch, destination, dispatched);
655            }
656        }
657
658        return redispatch;
659    }
660
661    private void addReferencesAndUpdateRedispatch(LinkedList<MessageReference> redispatch, Destination destination, List<MessageReference> dispatched) {
662        ArrayList<MessageReference> references = new ArrayList<MessageReference>();
663        for (MessageReference r : dispatched) {
664            if (r.getRegionDestination() == destination) {
665                references.add(r);
666                getSubscriptionStatistics().getInflightMessageSize().addSize(-r.getSize());
667            }
668        }
669        redispatch.addAll(0, references);
670        destination.getDestinationStatistics().getInflight().subtract(references.size());
671        dispatched.removeAll(references);
672    }
673
674    // made public so it can be used in MQTTProtocolConverter
675    public void dispatchPending() throws IOException {
676        List<Destination> slowConsumerTargets = null;
677
678        synchronized(pendingLock) {
679            try {
680                int numberToDispatch = countBeforeFull();
681                if (numberToDispatch > 0) {
682                    setSlowConsumer(false);
683                    setPendingBatchSize(pending, numberToDispatch);
684                    int count = 0;
685                    pending.reset();
686                    while (count < numberToDispatch && !isFull() && pending.hasNext()) {
687                        MessageReference node = pending.next();
688                        if (node == null) {
689                            break;
690                        }
691
692                        // Synchronize between dispatched list and remove of message from pending list
693                        // related to remove subscription action
694                        synchronized(dispatchLock) {
695                            pending.remove();
696                            if (!isDropped(node) && canDispatch(node)) {
697
698                                // Message may have been sitting in the pending
699                                // list a while waiting for the consumer to ak the message.
700                                if (node != QueueMessageReference.NULL_MESSAGE && node.isExpired()) {
701                                    //increment number to dispatch
702                                    numberToDispatch++;
703                                    if (broker.isExpired(node)) {
704                                        ((Destination)node.getRegionDestination()).messageExpired(context, this, node);
705                                    }
706
707                                    if (!isBrowser()) {
708                                        node.decrementReferenceCount();
709                                        continue;
710                                    }
711                                }
712                                dispatch(node);
713                                count++;
714                            }
715                        }
716                        // decrement after dispatch has taken ownership to avoid usage jitter
717                        node.decrementReferenceCount();
718                    }
719                } else if (!isSlowConsumer()) {
720                    setSlowConsumer(true);
721                    slowConsumerTargets = destinations;
722                }
723            } finally {
724                pending.release();
725            }
726        }
727
728        if (slowConsumerTargets != null) {
729            for (Destination dest : slowConsumerTargets) {
730                dest.slowConsumer(context, this);
731            }
732        }
733    }
734
735    protected void setPendingBatchSize(PendingMessageCursor pending, int numberToDispatch) {
736        pending.setMaxBatchSize(numberToDispatch);
737    }
738
739    // called with dispatchLock held
740    protected boolean dispatch(final MessageReference node) throws IOException {
741        final Message message = node.getMessage();
742        if (message == null) {
743            return false;
744        }
745
746        okForAckAsDispatchDone.countDown();
747
748        MessageDispatch md = createMessageDispatch(node, message);
749        if (node != QueueMessageReference.NULL_MESSAGE) {
750            getSubscriptionStatistics().getDispatched().increment();
751            dispatched.add(node);
752            getSubscriptionStatistics().getInflightMessageSize().addSize(node.getSize());
753        }
754        if (getPrefetchSize() == 0) {
755            while (true) {
756                int currentExtension = prefetchExtension.get();
757                int newExtension = Math.max(0, currentExtension - 1);
758                if (prefetchExtension.compareAndSet(currentExtension, newExtension)) {
759                    break;
760                }
761            }
762        }
763        if (info.isDispatchAsync()) {
764            md.setTransmitCallback(new TransmitCallback() {
765
766                @Override
767                public void onSuccess() {
768                    // Since the message gets queued up in async dispatch, we don't want to
769                    // decrease the reference count until it gets put on the wire.
770                    onDispatch(node, message);
771                }
772
773                @Override
774                public void onFailure() {
775                    Destination nodeDest = (Destination) node.getRegionDestination();
776                    if (nodeDest != null) {
777                        if (node != QueueMessageReference.NULL_MESSAGE) {
778                            nodeDest.getDestinationStatistics().getDispatched().increment();
779                            nodeDest.getDestinationStatistics().getInflight().increment();
780                            LOG.trace("{} failed to dispatch: {} - {}, dispatched: {}, inflight: {}", new Object[]{ info.getConsumerId(), message.getMessageId(), message.getDestination(), getSubscriptionStatistics().getDispatched().getCount(), dispatched.size() });
781                        }
782                    }
783                    if (node instanceof QueueMessageReference) {
784                        ((QueueMessageReference) node).unlock();
785                    }
786                }
787            });
788            context.getConnection().dispatchAsync(md);
789        } else {
790            context.getConnection().dispatchSync(md);
791            onDispatch(node, message);
792        }
793        return true;
794    }
795
796    protected void onDispatch(final MessageReference node, final Message message) {
797        Destination nodeDest = (Destination) node.getRegionDestination();
798        if (nodeDest != null) {
799            if (node != QueueMessageReference.NULL_MESSAGE) {
800                nodeDest.getDestinationStatistics().getDispatched().increment();
801                nodeDest.getDestinationStatistics().getInflight().increment();
802                LOG.trace("{} dispatched: {} - {}, dispatched: {}, inflight: {}", new Object[]{ info.getConsumerId(), message.getMessageId(), message.getDestination(), getSubscriptionStatistics().getDispatched().getCount(), dispatched.size() });
803            }
804        }
805
806        if (info.isDispatchAsync()) {
807            try {
808                dispatchPending();
809            } catch (IOException e) {
810                context.getConnection().serviceExceptionAsync(e);
811            }
812        }
813    }
814
815    /**
816     * inform the MessageConsumer on the client to change it's prefetch
817     *
818     * @param newPrefetch
819     */
820    @Override
821    public void updateConsumerPrefetch(int newPrefetch) {
822        if (context != null && context.getConnection() != null && context.getConnection().isManageable()) {
823            ConsumerControl cc = new ConsumerControl();
824            cc.setConsumerId(info.getConsumerId());
825            cc.setPrefetch(newPrefetch);
826            context.getConnection().dispatchAsync(cc);
827        }
828    }
829
830    /**
831     * @param node
832     * @param message
833     * @return MessageDispatch
834     */
835    protected MessageDispatch createMessageDispatch(MessageReference node, Message message) {
836        MessageDispatch md = new MessageDispatch();
837        md.setConsumerId(info.getConsumerId());
838
839        if (node == QueueMessageReference.NULL_MESSAGE) {
840            md.setMessage(null);
841            md.setDestination(null);
842        } else {
843            Destination regionDestination = (Destination) node.getRegionDestination();
844            md.setDestination(regionDestination.getActiveMQDestination());
845            md.setMessage(message);
846            md.setRedeliveryCounter(node.getRedeliveryCounter());
847        }
848
849        return md;
850    }
851
852    /**
853     * Use when a matched message is about to be dispatched to the client.
854     *
855     * @param node
856     * @return false if the message should not be dispatched to the client
857     *         (another sub may have already dispatched it for example).
858     * @throws IOException
859     */
860    protected abstract boolean canDispatch(MessageReference node) throws IOException;
861
862    protected abstract boolean isDropped(MessageReference node);
863
864    /**
865     * Used during acknowledgment to remove the message.
866     *
867     * @throws IOException
868     */
869    protected abstract void acknowledge(ConnectionContext context, final MessageAck ack, final MessageReference node) throws IOException;
870
871
872    public int getMaxProducersToAudit() {
873        return maxProducersToAudit;
874    }
875
876    public void setMaxProducersToAudit(int maxProducersToAudit) {
877        this.maxProducersToAudit = maxProducersToAudit;
878        if (this.pending != null) {
879            this.pending.setMaxProducersToAudit(maxProducersToAudit);
880        }
881    }
882
883    public int getMaxAuditDepth() {
884        return maxAuditDepth;
885    }
886
887    public void setMaxAuditDepth(int maxAuditDepth) {
888        this.maxAuditDepth = maxAuditDepth;
889        if (this.pending != null) {
890            this.pending.setMaxAuditDepth(maxAuditDepth);
891        }
892    }
893
894    @Override
895    public void setPrefetchSize(int prefetchSize) {
896        this.info.setPrefetchSize(prefetchSize);
897        try {
898            this.dispatchPending();
899        } catch (Exception e) {
900            LOG.trace("Caught exception during dispatch after prefetch change.", e);
901        }
902    }
903}