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