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                            onDispatch(node, node.getMessage());
178                        }
179                        return;
180                    }
181                }
182            } finally {
183                pending.release();
184            }
185        }
186        throw new JMSException(
187                "Slave broker out of sync with master: Dispatched message ("
188                        + mdn.getMessageId() + ") was not in the pending list for "
189                        + mdn.getConsumerId() + " on " + mdn.getDestination().getPhysicalName());
190    }
191
192    @Override
193    public final void acknowledge(final ConnectionContext context,final MessageAck ack) throws Exception {
194        // Handle the standard acknowledgment case.
195        boolean callDispatchMatched = false;
196        Destination destination = null;
197
198        if (!okForAckAsDispatchDone.await(0l, TimeUnit.MILLISECONDS)) {
199            // suppress unexpected ack exception in this expected case
200            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);
201            return;
202        }
203
204        LOG.trace("ack: {}", ack);
205
206        synchronized(dispatchLock) {
207            if (ack.isStandardAck()) {
208                // First check if the ack matches the dispatched. When using failover this might
209                // not be the case. We don't ever want to ack the wrong messages.
210                assertAckMatchesDispatched(ack);
211
212                // Acknowledge all dispatched messages up till the message id of
213                // the acknowledgment.
214                boolean inAckRange = false;
215                List<MessageReference> removeList = new ArrayList<MessageReference>();
216                for (final MessageReference node : dispatched) {
217                    MessageId messageId = node.getMessageId();
218                    if (ack.getFirstMessageId() == null
219                            || ack.getFirstMessageId().equals(messageId)) {
220                        inAckRange = true;
221                    }
222                    if (inAckRange) {
223                        // Don't remove the nodes until we are committed.
224                        if (!context.isInTransaction()) {
225                            getSubscriptionStatistics().getDequeues().increment();
226                            removeList.add(node);
227                            contractPrefetchExtension(1);
228                        } else {
229                            registerRemoveSync(context, node);
230                        }
231                        acknowledge(context, ack, node);
232                        if (ack.getLastMessageId().equals(messageId)) {
233                            destination = (Destination) node.getRegionDestination();
234                            callDispatchMatched = true;
235                            break;
236                        }
237                    }
238                }
239                for (final MessageReference node : removeList) {
240                    dispatched.remove(node);
241                    decrementPrefetchCounter(node);
242                }
243                // this only happens after a reconnect - get an ack which is not
244                // valid
245                if (!callDispatchMatched) {
246                    LOG.warn("Could not correlate acknowledgment with dispatched message: {}", ack);
247                }
248            } else if (ack.isIndividualAck()) {
249                // Message was delivered and acknowledge - but only delete the
250                // individual message
251                for (final MessageReference node : dispatched) {
252                    MessageId messageId = node.getMessageId();
253                    if (ack.getLastMessageId().equals(messageId)) {
254                        // Don't remove the nodes until we are committed - immediateAck option
255                        if (!context.isInTransaction()) {
256                            getSubscriptionStatistics().getDequeues().increment();
257                            dispatched.remove(node);
258                            decrementPrefetchCounter(node);
259                            contractPrefetchExtension(1);
260                        } else {
261                            registerRemoveSync(context, node);
262                            expandPrefetchExtension(1);
263                        }
264                        acknowledge(context, ack, node);
265                        destination = (Destination) node.getRegionDestination();
266                        callDispatchMatched = true;
267                        break;
268                    }
269                }
270            } else if (ack.isDeliveredAck()) {
271                // Message was delivered but not acknowledged: update pre-fetch
272                // counters.
273                int index = 0;
274                for (Iterator<MessageReference> iter = dispatched.iterator(); iter.hasNext(); index++) {
275                    final MessageReference node = iter.next();
276                    Destination nodeDest = (Destination) node.getRegionDestination();
277                    if (ack.getLastMessageId().equals(node.getMessageId())) {
278                        expandPrefetchExtension(ack.getMessageCount());
279                        destination = nodeDest;
280                        callDispatchMatched = true;
281                        break;
282                    }
283                }
284                if (!callDispatchMatched) {
285                    throw new JMSException(
286                            "Could not correlate acknowledgment with dispatched message: "
287                                    + ack);
288                }
289            } else if (ack.isExpiredAck()) {
290                // Message was expired
291                int index = 0;
292                boolean inAckRange = false;
293                for (Iterator<MessageReference> iter = dispatched.iterator(); iter.hasNext(); index++) {
294                    final MessageReference node = iter.next();
295                    Destination nodeDest = (Destination) node.getRegionDestination();
296                    MessageId messageId = node.getMessageId();
297                    if (ack.getFirstMessageId() == null || ack.getFirstMessageId().equals(messageId)) {
298                        inAckRange = true;
299                    }
300                    if (inAckRange) {
301                        Destination regionDestination = nodeDest;
302                        if (broker.isExpired(node)) {
303                            regionDestination.messageExpired(context, this, node);
304                        }
305                        iter.remove();
306                        decrementPrefetchCounter(node);
307
308                        if (ack.getLastMessageId().equals(messageId)) {
309                            contractPrefetchExtension(1);
310                            destination = (Destination) node.getRegionDestination();
311                            callDispatchMatched = true;
312                            break;
313                        }
314                    }
315                }
316                if (!callDispatchMatched) {
317                    throw new JMSException(
318                            "Could not correlate expiration acknowledgment with dispatched message: "
319                                    + ack);
320                }
321            } else if (ack.isRedeliveredAck()) {
322                // Message was re-delivered but it was not yet considered to be
323                // a DLQ message.
324                boolean inAckRange = false;
325                for (final MessageReference node : dispatched) {
326                    MessageId messageId = node.getMessageId();
327                    if (ack.getFirstMessageId() == null
328                            || ack.getFirstMessageId().equals(messageId)) {
329                        inAckRange = true;
330                    }
331                    if (inAckRange) {
332                        if (ack.getLastMessageId().equals(messageId)) {
333                            destination = (Destination) node.getRegionDestination();
334                            callDispatchMatched = true;
335                            break;
336                        }
337                    }
338                }
339                if (!callDispatchMatched) {
340                    throw new JMSException(
341                            "Could not correlate acknowledgment with dispatched message: "
342                                    + ack);
343                }
344            } else if (ack.isPoisonAck()) {
345                // TODO: what if the message is already in a DLQ???
346                // Handle the poison ACK case: we need to send the message to a
347                // DLQ
348                if (ack.isInTransaction()) {
349                    throw new JMSException("Poison ack cannot be transacted: "
350                            + ack);
351                }
352                int index = 0;
353                boolean inAckRange = false;
354                List<MessageReference> removeList = new ArrayList<MessageReference>();
355                for (final MessageReference node : dispatched) {
356                    MessageId messageId = node.getMessageId();
357                    if (ack.getFirstMessageId() == null
358                            || ack.getFirstMessageId().equals(messageId)) {
359                        inAckRange = true;
360                    }
361                    if (inAckRange) {
362                        sendToDLQ(context, node, ack.getPoisonCause());
363                        Destination nodeDest = (Destination) node.getRegionDestination();
364                        removeList.add(node);
365                        getSubscriptionStatistics().getDequeues().increment();
366                        index++;
367                        acknowledge(context, ack, node);
368                        if (ack.getLastMessageId().equals(messageId)) {
369                            contractPrefetchExtension(1);
370                            destination = nodeDest;
371                            callDispatchMatched = true;
372                            break;
373                        }
374                    }
375                }
376                for (final MessageReference node : removeList) {
377                    dispatched.remove(node);
378                    decrementPrefetchCounter(node);
379                }
380                if (!callDispatchMatched) {
381                    throw new JMSException(
382                            "Could not correlate acknowledgment with dispatched message: "
383                                    + ack);
384                }
385            }
386        }
387        if (callDispatchMatched && destination != null) {
388            destination.wakeup();
389            dispatchPending();
390
391            if (pending.isEmpty()) {
392                wakeupDestinationsForDispatch();
393            }
394        } else {
395            LOG.debug("Acknowledgment out of sync (Normally occurs when failover connection reconnects): {}", ack);
396        }
397    }
398
399    private void registerRemoveSync(ConnectionContext context, final MessageReference node) {
400        // setup a Synchronization to remove nodes from the
401        // dispatched list.
402        context.getTransaction().addSynchronization(
403                new Synchronization() {
404
405                    @Override
406                    public void afterCommit()
407                            throws Exception {
408                        Destination nodeDest = (Destination) node.getRegionDestination();
409                        synchronized (dispatchLock) {
410                            getSubscriptionStatistics().getDequeues().increment();
411                            if (dispatched.remove(node)) {
412                                // if consumer is removed, dispatched will be empty and inflight will
413                                // already have been adjusted
414                                decrementPrefetchCounter(node);
415                            }
416                        }
417                        contractPrefetchExtension(1);
418                        nodeDest.wakeup();
419                        dispatchPending();
420                    }
421
422                    @Override
423                    public void afterRollback() throws Exception {
424                        contractPrefetchExtension(1);
425                    }
426                });
427    }
428
429    /**
430     * Checks an ack versus the contents of the dispatched list.
431     *  called with dispatchLock held
432     * @param ack
433     * @throws JMSException if it does not match
434     */
435    protected void assertAckMatchesDispatched(MessageAck ack) throws JMSException {
436        MessageId firstAckedMsg = ack.getFirstMessageId();
437        MessageId lastAckedMsg = ack.getLastMessageId();
438        int checkCount = 0;
439        boolean checkFoundStart = false;
440        boolean checkFoundEnd = false;
441        for (MessageReference node : dispatched) {
442
443            if (firstAckedMsg == null) {
444                checkFoundStart = true;
445            } else if (!checkFoundStart && firstAckedMsg.equals(node.getMessageId())) {
446                checkFoundStart = true;
447            }
448
449            if (checkFoundStart) {
450                checkCount++;
451            }
452
453            if (lastAckedMsg != null && lastAckedMsg.equals(node.getMessageId())) {
454                checkFoundEnd = true;
455                break;
456            }
457        }
458        if (!checkFoundStart && firstAckedMsg != null)
459            throw new JMSException("Unmatched acknowledge: " + ack
460                    + "; Could not find Message-ID " + firstAckedMsg
461                    + " in dispatched-list (start of ack)");
462        if (!checkFoundEnd && lastAckedMsg != null)
463            throw new JMSException("Unmatched acknowledge: " + ack
464                    + "; Could not find Message-ID " + lastAckedMsg
465                    + " in dispatched-list (end of ack)");
466        if (ack.getMessageCount() != checkCount && !ack.isInTransaction()) {
467            throw new JMSException("Unmatched acknowledge: " + ack
468                    + "; Expected message count (" + ack.getMessageCount()
469                    + ") differs from count in dispatched-list (" + checkCount
470                    + ")");
471        }
472    }
473
474    /**
475     *
476     * @param context
477     * @param node
478     * @param poisonCause
479     * @throws IOException
480     * @throws Exception
481     */
482    protected void sendToDLQ(final ConnectionContext context, final MessageReference node, Throwable poisonCause) throws IOException, Exception {
483        broker.getRoot().sendToDeadLetterQueue(context, node, this, poisonCause);
484    }
485
486    @Override
487    public int getInFlightSize() {
488        return dispatched.size();
489    }
490
491    /**
492     * Used to determine if the broker can dispatch to the consumer.
493     *
494     * @return true if the subscription is full
495     */
496    @Override
497    public boolean isFull() {
498        return getPrefetchSize() == 0 ? prefetchExtension.get() == 0 : dispatched.size() - prefetchExtension.get() >= info.getPrefetchSize();
499    }
500
501    /**
502     * @return true when 60% or more room is left for dispatching messages
503     */
504    @Override
505    public boolean isLowWaterMark() {
506        return (dispatched.size() - prefetchExtension.get()) <= (info.getPrefetchSize() * .4);
507    }
508
509    /**
510     * @return true when 10% or less room is left for dispatching messages
511     */
512    @Override
513    public boolean isHighWaterMark() {
514        return (dispatched.size() - prefetchExtension.get()) >= (info.getPrefetchSize() * .9);
515    }
516
517    @Override
518    public int countBeforeFull() {
519        return getPrefetchSize() == 0 ? prefetchExtension.get() : info.getPrefetchSize() + prefetchExtension.get() - dispatched.size();
520    }
521
522    @Override
523    public int getPendingQueueSize() {
524        return pending.size();
525    }
526
527    @Override
528    public long getPendingMessageSize() {
529        return pending.messageSize();
530    }
531
532    @Override
533    public int getDispatchedQueueSize() {
534        return dispatched.size();
535    }
536
537    @Override
538    public long getDequeueCounter() {
539        return getSubscriptionStatistics().getDequeues().getCount();
540    }
541
542    @Override
543    public long getDispatchedCounter() {
544        return getSubscriptionStatistics().getDispatched().getCount();
545    }
546
547    @Override
548    public long getEnqueueCounter() {
549        return getSubscriptionStatistics().getEnqueues().getCount();
550    }
551
552    @Override
553    public boolean isRecoveryRequired() {
554        return pending.isRecoveryRequired();
555    }
556
557    public PendingMessageCursor getPending() {
558        return this.pending;
559    }
560
561    public void setPending(PendingMessageCursor pending) {
562        this.pending = pending;
563        if (this.pending!=null) {
564            this.pending.setSystemUsage(usageManager);
565            this.pending.setMemoryUsageHighWaterMark(getCursorMemoryHighWaterMark());
566        }
567    }
568
569    @Override
570    public void add(ConnectionContext context, Destination destination) throws Exception {
571        synchronized(pendingLock) {
572            super.add(context, destination);
573            pending.add(context, destination);
574        }
575    }
576
577    @Override
578    public List<MessageReference> remove(ConnectionContext context, Destination destination) throws Exception {
579        return remove(context, destination, dispatched);
580    }
581
582    public List<MessageReference> remove(ConnectionContext context, Destination destination, List<MessageReference> dispatched) throws Exception {
583        LinkedList<MessageReference> redispatch = new LinkedList<MessageReference>();
584        synchronized(pendingLock) {
585            super.remove(context, destination);
586            // Here is a potential problem concerning Inflight stat:
587            // Messages not already committed or rolled back may not be removed from dispatched list at the moment
588            // Except if each commit or rollback callback action comes before remove of subscriber.
589            redispatch.addAll(pending.remove(context, destination));
590
591            if (dispatched == null) {
592                return redispatch;
593            }
594
595            // Synchronized to DispatchLock if necessary
596            if (dispatched == this.dispatched) {
597                synchronized(dispatchLock) {
598                    addReferencesAndUpdateRedispatch(redispatch, destination, dispatched);
599                }
600            } else {
601                addReferencesAndUpdateRedispatch(redispatch, destination, dispatched);
602            }
603        }
604
605        return redispatch;
606    }
607
608    private void addReferencesAndUpdateRedispatch(LinkedList<MessageReference> redispatch, Destination destination, List<MessageReference> dispatched) {
609        ArrayList<MessageReference> references = new ArrayList<MessageReference>();
610        for (MessageReference r : dispatched) {
611            if (r.getRegionDestination() == destination) {
612                references.add(r);
613                getSubscriptionStatistics().getInflightMessageSize().addSize(-r.getSize());
614            }
615        }
616        redispatch.addAll(0, references);
617        destination.getDestinationStatistics().getInflight().subtract(references.size());
618        dispatched.removeAll(references);
619    }
620
621    // made public so it can be used in MQTTProtocolConverter
622    public void dispatchPending() throws IOException {
623        List<Destination> slowConsumerTargets = null;
624
625        synchronized(pendingLock) {
626            try {
627                int numberToDispatch = countBeforeFull();
628                if (numberToDispatch > 0) {
629                    setSlowConsumer(false);
630                    setPendingBatchSize(pending, numberToDispatch);
631                    int count = 0;
632                    pending.reset();
633                    while (count < numberToDispatch && !isFull() && pending.hasNext()) {
634                        MessageReference node = pending.next();
635                        if (node == null) {
636                            break;
637                        }
638                        if (trackedInPendingTransaction(node)) {
639                            node.decrementReferenceCount();
640                            continue;
641                        }
642
643                        // Synchronize between dispatched list and remove of message from pending list
644                        // related to remove subscription action
645                        synchronized(dispatchLock) {
646                            pending.remove();
647                            if (!isDropped(node) && canDispatch(node)) {
648
649                                // Message may have been sitting in the pending
650                                // list a while waiting for the consumer to ak the message.
651                                if (node != QueueMessageReference.NULL_MESSAGE && node.isExpired()) {
652                                    //increment number to dispatch
653                                    numberToDispatch++;
654                                    if (broker.isExpired(node)) {
655                                        ((Destination)node.getRegionDestination()).messageExpired(context, this, node);
656                                    }
657
658                                    if (!isBrowser()) {
659                                        node.decrementReferenceCount();
660                                        continue;
661                                    }
662                                }
663                                dispatch(node);
664                                count++;
665                            }
666                        }
667                        // decrement after dispatch has taken ownership to avoid usage jitter
668                        node.decrementReferenceCount();
669                    }
670                } else if (!pending.isEmpty() && !isSlowConsumer()) {
671                    setSlowConsumer(true);
672                    slowConsumerTargets = destinations;
673                }
674            } finally {
675                pending.release();
676            }
677        }
678
679        if (slowConsumerTargets != null) {
680            for (Destination dest : slowConsumerTargets) {
681                dest.slowConsumer(context, this);
682            }
683        }
684    }
685
686    protected boolean trackedInPendingTransaction(MessageReference node) {
687        return false;
688    }
689
690    protected void setPendingBatchSize(PendingMessageCursor pending, int numberToDispatch) {
691        pending.setMaxBatchSize(numberToDispatch);
692    }
693
694    // called with dispatchLock held
695    protected boolean dispatch(final MessageReference node) throws IOException {
696        final Message message = node.getMessage();
697        if (message == null) {
698            return false;
699        }
700
701        okForAckAsDispatchDone.countDown();
702
703        MessageDispatch md = createMessageDispatch(node, message);
704        if (node != QueueMessageReference.NULL_MESSAGE) {
705            dispatched.add(node);
706            getSubscriptionStatistics().getDispatched().increment();
707        }
708        if (getPrefetchSize() == 0) {
709            while (true) {
710                int currentExtension = prefetchExtension.get();
711                int newExtension = Math.max(0, currentExtension - 1);
712                if (prefetchExtension.compareAndSet(currentExtension, newExtension)) {
713                    break;
714                }
715            }
716        }
717        if (info.isDispatchAsync()) {
718            md.setTransmitCallback(new TransmitCallback() {
719
720                @Override
721                public void onSuccess() {
722                    // Since the message gets queued up in async dispatch, we don't want to
723                    // decrease the reference count until it gets put on the wire.
724                    onDispatch(node, message);
725                }
726
727                @Override
728                public void onFailure() {
729                    Destination nodeDest = (Destination) node.getRegionDestination();
730                    if (nodeDest != null) {
731                        if (node != QueueMessageReference.NULL_MESSAGE) {
732                            nodeDest.getDestinationStatistics().getDispatched().increment();
733                            incrementPrefetchCounter(node);
734                            LOG.trace("{} failed to dispatch: {} - {}, dispatched: {}, inflight: {}", new Object[]{ info.getConsumerId(), message.getMessageId(), message.getDestination(), getSubscriptionStatistics().getDispatched().getCount(), dispatched.size() });
735                        }
736                    }
737                    if (node instanceof QueueMessageReference) {
738                        ((QueueMessageReference) node).unlock();
739                    }
740                }
741            });
742            context.getConnection().dispatchAsync(md);
743        } else {
744            context.getConnection().dispatchSync(md);
745            onDispatch(node, message);
746        }
747        return true;
748    }
749
750    protected void onDispatch(final MessageReference node, final Message message) {
751        Destination nodeDest = (Destination) node.getRegionDestination();
752        if (nodeDest != null) {
753            if (node != QueueMessageReference.NULL_MESSAGE) {
754                nodeDest.getDestinationStatistics().getDispatched().increment();
755                incrementPrefetchCounter(node);
756                LOG.trace("{} dispatched: {} - {}, dispatched: {}, inflight: {}", new Object[]{ info.getConsumerId(), message.getMessageId(), message.getDestination(), getSubscriptionStatistics().getDispatched().getCount(), dispatched.size() });
757            }
758        }
759
760        if (info.isDispatchAsync()) {
761            try {
762                dispatchPending();
763            } catch (IOException e) {
764                context.getConnection().serviceExceptionAsync(e);
765            }
766        }
767    }
768
769    /**
770     * inform the MessageConsumer on the client to change it's prefetch
771     *
772     * @param newPrefetch
773     */
774    @Override
775    public void updateConsumerPrefetch(int newPrefetch) {
776        if (context != null && context.getConnection() != null && context.getConnection().isManageable()) {
777            ConsumerControl cc = new ConsumerControl();
778            cc.setConsumerId(info.getConsumerId());
779            cc.setPrefetch(newPrefetch);
780            context.getConnection().dispatchAsync(cc);
781        }
782    }
783
784    /**
785     * @param node
786     * @param message
787     * @return MessageDispatch
788     */
789    protected MessageDispatch createMessageDispatch(MessageReference node, Message message) {
790        MessageDispatch md = new MessageDispatch();
791        md.setConsumerId(info.getConsumerId());
792
793        if (node == QueueMessageReference.NULL_MESSAGE) {
794            md.setMessage(null);
795            md.setDestination(null);
796        } else {
797            Destination regionDestination = (Destination) node.getRegionDestination();
798            md.setDestination(regionDestination.getActiveMQDestination());
799            md.setMessage(message);
800            md.setRedeliveryCounter(node.getRedeliveryCounter());
801        }
802
803        return md;
804    }
805
806    /**
807     * Use when a matched message is about to be dispatched to the client.
808     *
809     * @param node
810     * @return false if the message should not be dispatched to the client
811     *         (another sub may have already dispatched it for example).
812     * @throws IOException
813     */
814    protected abstract boolean canDispatch(MessageReference node) throws IOException;
815
816    protected abstract boolean isDropped(MessageReference node);
817
818    /**
819     * Used during acknowledgment to remove the message.
820     *
821     * @throws IOException
822     */
823    protected abstract void acknowledge(ConnectionContext context, final MessageAck ack, final MessageReference node) throws IOException;
824
825
826    public int getMaxProducersToAudit() {
827        return maxProducersToAudit;
828    }
829
830    public void setMaxProducersToAudit(int maxProducersToAudit) {
831        this.maxProducersToAudit = maxProducersToAudit;
832        if (this.pending != null) {
833            this.pending.setMaxProducersToAudit(maxProducersToAudit);
834        }
835    }
836
837    public int getMaxAuditDepth() {
838        return maxAuditDepth;
839    }
840
841    public void setMaxAuditDepth(int maxAuditDepth) {
842        this.maxAuditDepth = maxAuditDepth;
843        if (this.pending != null) {
844            this.pending.setMaxAuditDepth(maxAuditDepth);
845        }
846    }
847
848    @Override
849    public void setPrefetchSize(int prefetchSize) {
850        this.info.setPrefetchSize(prefetchSize);
851        try {
852            this.dispatchPending();
853        } catch (Exception e) {
854            LOG.trace("Caught exception during dispatch after prefetch change.", e);
855        }
856    }
857
858    private void incrementPrefetchCounter(final MessageReference node) {
859        ((Destination)node.getRegionDestination()).getDestinationStatistics().getInflight().increment();
860        getSubscriptionStatistics().getInflightMessageSize().addSize(node.getSize());
861    }
862
863    private void decrementPrefetchCounter(final MessageReference node) {
864        ((Destination)node.getRegionDestination()).getDestinationStatistics().getInflight().decrement();
865        getSubscriptionStatistics().getInflightMessageSize().addSize(-node.getSize());
866    }
867}