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.LinkedList;
022import java.util.List;
023import java.util.concurrent.atomic.AtomicInteger;
024import java.util.concurrent.atomic.AtomicLong;
025
026import javax.jms.JMSException;
027
028import org.apache.activemq.ActiveMQMessageAudit;
029import org.apache.activemq.broker.Broker;
030import org.apache.activemq.broker.ConnectionContext;
031import org.apache.activemq.broker.region.cursors.FilePendingMessageCursor;
032import org.apache.activemq.broker.region.cursors.PendingMessageCursor;
033import org.apache.activemq.broker.region.cursors.VMPendingMessageCursor;
034import org.apache.activemq.broker.region.policy.MessageEvictionStrategy;
035import org.apache.activemq.broker.region.policy.OldestMessageEvictionStrategy;
036import org.apache.activemq.command.ConsumerControl;
037import org.apache.activemq.command.ConsumerInfo;
038import org.apache.activemq.command.Message;
039import org.apache.activemq.command.MessageAck;
040import org.apache.activemq.command.MessageDispatch;
041import org.apache.activemq.command.MessageDispatchNotification;
042import org.apache.activemq.command.MessageId;
043import org.apache.activemq.command.MessagePull;
044import org.apache.activemq.command.Response;
045import org.apache.activemq.thread.Scheduler;
046import org.apache.activemq.transaction.Synchronization;
047import org.apache.activemq.transport.TransmitCallback;
048import org.apache.activemq.usage.SystemUsage;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051
052public class TopicSubscription extends AbstractSubscription {
053
054    private static final Logger LOG = LoggerFactory.getLogger(TopicSubscription.class);
055    private static final AtomicLong CURSOR_NAME_COUNTER = new AtomicLong(0);
056
057    protected PendingMessageCursor matched;
058    protected final SystemUsage usageManager;
059    boolean singleDestination = true;
060    Destination destination;
061    private final Scheduler scheduler;
062
063    private int maximumPendingMessages = -1;
064    private MessageEvictionStrategy messageEvictionStrategy = new OldestMessageEvictionStrategy();
065    private int discarded;
066    private final Object matchedListMutex = new Object();
067    private final AtomicInteger prefetchExtension = new AtomicInteger(0);
068    private int memoryUsageHighWaterMark = 95;
069    // allow duplicate suppression in a ring network of brokers
070    protected int maxProducersToAudit = 1024;
071    protected int maxAuditDepth = 1000;
072    protected boolean enableAudit = false;
073    protected ActiveMQMessageAudit audit;
074    protected boolean active = false;
075    protected boolean discarding = false;
076
077    //Used for inflight message size calculations
078    protected final Object dispatchLock = new Object();
079    protected final List<MessageReference> dispatched = new ArrayList<MessageReference>();
080
081    public TopicSubscription(Broker broker,ConnectionContext context, ConsumerInfo info, SystemUsage usageManager) throws Exception {
082        super(broker, context, info);
083        this.usageManager = usageManager;
084        String matchedName = "TopicSubscription:" + CURSOR_NAME_COUNTER.getAndIncrement() + "[" + info.getConsumerId().toString() + "]";
085        if (info.getDestination().isTemporary() || broker.getTempDataStore()==null ) {
086            this.matched = new VMPendingMessageCursor(false);
087        } else {
088            this.matched = new FilePendingMessageCursor(broker,matchedName,false);
089        }
090
091        this.scheduler = broker.getScheduler();
092    }
093
094    public void init() throws Exception {
095        this.matched.setSystemUsage(usageManager);
096        this.matched.setMemoryUsageHighWaterMark(getCursorMemoryHighWaterMark());
097        this.matched.start();
098        if (enableAudit) {
099            audit= new ActiveMQMessageAudit(maxAuditDepth, maxProducersToAudit);
100        }
101        this.active=true;
102    }
103
104    @Override
105    public void add(MessageReference node) throws Exception {
106        if (isDuplicate(node)) {
107            return;
108        }
109        // Lets use an indirect reference so that we can associate a unique
110        // locator /w the message.
111        node = new IndirectMessageReference(node.getMessage());
112        getSubscriptionStatistics().getEnqueues().increment();
113        synchronized (matchedListMutex) {
114            // if this subscriber is already discarding a message, we don't want to add
115            // any more messages to it as those messages can only be advisories generated in the process,
116            // which can trigger the recursive call loop
117            if (discarding) return;
118
119            if (!isFull() && matched.isEmpty()) {
120                // if maximumPendingMessages is set we will only discard messages which
121                // have not been dispatched (i.e. we allow the prefetch buffer to be filled)
122                dispatch(node);
123                setSlowConsumer(false);
124            } else {
125                if (info.getPrefetchSize() > 1 && matched.size() > info.getPrefetchSize()) {
126                    // Slow consumers should log and set their state as such.
127                    if (!isSlowConsumer()) {
128                        LOG.warn("{}: has twice its prefetch limit pending, without an ack; it appears to be slow", toString());
129                        setSlowConsumer(true);
130                        for (Destination dest: destinations) {
131                            dest.slowConsumer(getContext(), this);
132                        }
133                    }
134                }
135                if (maximumPendingMessages != 0) {
136                    boolean warnedAboutWait = false;
137                    while (active) {
138                        while (matched.isFull()) {
139                            if (getContext().getStopping().get()) {
140                                LOG.warn("{}: stopped waiting for space in pendingMessage cursor for: {}", toString(), node.getMessageId());
141                                getSubscriptionStatistics().getEnqueues().decrement();
142                                return;
143                            }
144                            if (!warnedAboutWait) {
145                                LOG.info("{}: Pending message cursor [{}] is full, temp usag ({}%) or memory usage ({}%) limit reached, blocking message add() pending the release of resources.",
146                                        new Object[]{
147                                                toString(),
148                                                matched,
149                                                matched.getSystemUsage().getTempUsage().getPercentUsage(),
150                                                matched.getSystemUsage().getMemoryUsage().getPercentUsage()
151                                        });
152                                warnedAboutWait = true;
153                            }
154                            matchedListMutex.wait(20);
155                        }
156                        // Temporary storage could be full - so just try to add the message
157                        // see https://issues.apache.org/activemq/browse/AMQ-2475
158                        if (matched.tryAddMessageLast(node, 10)) {
159                            break;
160                        }
161                    }
162                    if (maximumPendingMessages > 0) {
163                        // calculate the high water mark from which point we
164                        // will eagerly evict expired messages
165                        int max = messageEvictionStrategy.getEvictExpiredMessagesHighWatermark();
166                        if (maximumPendingMessages > 0 && maximumPendingMessages < max) {
167                            max = maximumPendingMessages;
168                        }
169                        if (!matched.isEmpty() && matched.size() > max) {
170                            removeExpiredMessages();
171                        }
172                        // lets discard old messages as we are a slow consumer
173                        while (!matched.isEmpty() && matched.size() > maximumPendingMessages) {
174                            int pageInSize = matched.size() - maximumPendingMessages;
175                            // only page in a 1000 at a time - else we could blow the memory
176                            pageInSize = Math.max(1000, pageInSize);
177                            LinkedList<MessageReference> list = null;
178                            MessageReference[] oldMessages=null;
179                            synchronized(matched){
180                                list = matched.pageInList(pageInSize);
181                                oldMessages = messageEvictionStrategy.evictMessages(list);
182                                for (MessageReference ref : list) {
183                                    ref.decrementReferenceCount();
184                                }
185                            }
186                            int messagesToEvict = 0;
187                            if (oldMessages != null){
188                                messagesToEvict = oldMessages.length;
189                                for (int i = 0; i < messagesToEvict; i++) {
190                                    MessageReference oldMessage = oldMessages[i];
191                                    discard(oldMessage);
192                                }
193                            }
194                            // lets avoid an infinite loop if we are given a bad eviction strategy
195                            // for a bad strategy lets just not evict
196                            if (messagesToEvict == 0) {
197                                LOG.warn("No messages to evict returned for {} from eviction strategy: {} out of {} candidates", new Object[]{
198                                        destination, messageEvictionStrategy, list.size()
199                                });
200                                break;
201                            }
202                        }
203                    }
204                    dispatchMatched();
205                }
206            }
207        }
208    }
209
210    private boolean isDuplicate(MessageReference node) {
211        boolean duplicate = false;
212        if (enableAudit && audit != null) {
213            duplicate = audit.isDuplicate(node);
214            if (LOG.isDebugEnabled()) {
215                if (duplicate) {
216                    LOG.debug("{}, ignoring duplicate add: {}", this, node.getMessageId());
217                }
218            }
219        }
220        return duplicate;
221    }
222
223    /**
224     * Discard any expired messages from the matched list. Called from a
225     * synchronized block.
226     *
227     * @throws IOException
228     */
229    protected void removeExpiredMessages() throws IOException {
230        try {
231            matched.reset();
232            while (matched.hasNext()) {
233                MessageReference node = matched.next();
234                node.decrementReferenceCount();
235                if (broker.isExpired(node)) {
236                    matched.remove();
237                    getSubscriptionStatistics().getDispatched().increment();
238                    node.decrementReferenceCount();
239                    ((Destination)node.getRegionDestination()).getDestinationStatistics().getExpired().increment();
240                    broker.messageExpired(getContext(), node, this);
241                    break;
242                }
243            }
244        } finally {
245            matched.release();
246        }
247    }
248
249    @Override
250    public void processMessageDispatchNotification(MessageDispatchNotification mdn) {
251        synchronized (matchedListMutex) {
252            try {
253                matched.reset();
254                while (matched.hasNext()) {
255                    MessageReference node = matched.next();
256                    node.decrementReferenceCount();
257                    if (node.getMessageId().equals(mdn.getMessageId())) {
258                        synchronized(dispatchLock) {
259                            matched.remove();
260                            getSubscriptionStatistics().getDispatched().increment();
261                            dispatched.add(node);
262                            getSubscriptionStatistics().getInflightMessageSize().addSize(node.getSize());
263                            node.decrementReferenceCount();
264                        }
265                        break;
266                    }
267                }
268            } finally {
269                matched.release();
270            }
271        }
272    }
273
274    @Override
275    public synchronized void acknowledge(final ConnectionContext context, final MessageAck ack) throws Exception {
276        super.acknowledge(context, ack);
277
278        // Handle the standard acknowledgment case.
279        if (ack.isStandardAck() || ack.isPoisonAck() || ack.isIndividualAck()) {
280            if (context.isInTransaction()) {
281                context.getTransaction().addSynchronization(new Synchronization() {
282                    @Override
283                    public void afterCommit() throws Exception {
284                        updateStatsOnAck(ack);
285                        dispatchMatched();
286                    }
287                });
288            } else {
289                updateStatsOnAck(ack);
290            }
291            updatePrefetch(ack);
292            dispatchMatched();
293            return;
294        } else if (ack.isDeliveredAck()) {
295            // Message was delivered but not acknowledged: update pre-fetch counters.
296            prefetchExtension.addAndGet(ack.getMessageCount());
297            dispatchMatched();
298            return;
299        } else if (ack.isExpiredAck()) {
300            updateStatsOnAck(ack);
301            updatePrefetch(ack);
302            dispatchMatched();
303            return;
304        } else if (ack.isRedeliveredAck()) {
305            // nothing to do atm
306            return;
307        }
308        throw new JMSException("Invalid acknowledgment: " + ack);
309    }
310
311    @Override
312    public Response pullMessage(ConnectionContext context, final MessagePull pull) throws Exception {
313
314        // The slave should not deliver pull messages.
315        if (getPrefetchSize() == 0) {
316
317            final long currentDispatchedCount = getSubscriptionStatistics().getDispatched().getCount();
318            prefetchExtension.set(pull.getQuantity());
319            dispatchMatched();
320
321            // If there was nothing dispatched.. we may need to setup a timeout.
322            if (currentDispatchedCount == getSubscriptionStatistics().getDispatched().getCount() || pull.isAlwaysSignalDone()) {
323
324                // immediate timeout used by receiveNoWait()
325                if (pull.getTimeout() == -1) {
326                    // Send a NULL message to signal nothing pending.
327                    dispatch(null);
328                    prefetchExtension.set(0);
329                }
330
331                if (pull.getTimeout() > 0) {
332                    scheduler.executeAfterDelay(new Runnable() {
333
334                        @Override
335                        public void run() {
336                            pullTimeout(currentDispatchedCount, pull.isAlwaysSignalDone());
337                        }
338                    }, pull.getTimeout());
339                }
340            }
341        }
342        return null;
343    }
344
345    /**
346     * Occurs when a pull times out. If nothing has been dispatched since the
347     * timeout was setup, then send the NULL message.
348     */
349    private final void pullTimeout(long currentDispatchedCount, boolean alwaysSendDone) {
350        synchronized (matchedListMutex) {
351            if (currentDispatchedCount == getSubscriptionStatistics().getDispatched().getCount() || alwaysSendDone) {
352                try {
353                    dispatch(null);
354                } catch (Exception e) {
355                    context.getConnection().serviceException(e);
356                } finally {
357                    prefetchExtension.set(0);
358                }
359            }
360        }
361    }
362
363    /**
364     * Update the statistics on message ack.
365     * @param ack
366     */
367    private void updateStatsOnAck(final MessageAck ack) {
368        synchronized(dispatchLock) {
369            boolean inAckRange = false;
370            List<MessageReference> removeList = new ArrayList<MessageReference>();
371            for (final MessageReference node : dispatched) {
372                MessageId messageId = node.getMessageId();
373                if (ack.getFirstMessageId() == null
374                        || ack.getFirstMessageId().equals(messageId)) {
375                    inAckRange = true;
376                }
377                if (inAckRange) {
378                    removeList.add(node);
379                    if (ack.getLastMessageId().equals(messageId)) {
380                        break;
381                    }
382                }
383            }
384
385            for (final MessageReference node : removeList) {
386                dispatched.remove(node);
387                getSubscriptionStatistics().getInflightMessageSize().addSize(-node.getSize());
388                getSubscriptionStatistics().getDequeues().increment();
389                ((Destination)node.getRegionDestination()).getDestinationStatistics().getDequeues().increment();
390                ((Destination)node.getRegionDestination()).getDestinationStatistics().getInflight().decrement();
391                if (info.isNetworkSubscription()) {
392                    ((Destination)node.getRegionDestination()).getDestinationStatistics().getForwards().add(ack.getMessageCount());
393                }
394                if (ack.isExpiredAck()) {
395                    destination.getDestinationStatistics().getExpired().add(ack.getMessageCount());
396                }
397            }
398        }
399    }
400
401    private void updatePrefetch(MessageAck ack) {
402        while (true) {
403            int currentExtension = prefetchExtension.get();
404            int newExtension = Math.max(0, currentExtension - ack.getMessageCount());
405            if (prefetchExtension.compareAndSet(currentExtension, newExtension)) {
406                break;
407            }
408        }
409    }
410
411    @Override
412    public int getPendingQueueSize() {
413        return matched();
414    }
415
416    @Override
417    public int getDispatchedQueueSize() {
418        return (int)(getSubscriptionStatistics().getDispatched().getCount() -
419                prefetchExtension.get() - getSubscriptionStatistics().getDequeues().getCount());
420    }
421
422    public int getMaximumPendingMessages() {
423        return maximumPendingMessages;
424    }
425
426    @Override
427    public long getDispatchedCounter() {
428        return getSubscriptionStatistics().getDispatched().getCount();
429    }
430
431    @Override
432    public long getEnqueueCounter() {
433        return getSubscriptionStatistics().getEnqueues().getCount();
434    }
435
436    @Override
437    public long getDequeueCounter() {
438        return getSubscriptionStatistics().getDequeues().getCount();
439    }
440
441    /**
442     * @return the number of messages discarded due to being a slow consumer
443     */
444    public int discarded() {
445        synchronized (matchedListMutex) {
446            return discarded;
447        }
448    }
449
450    /**
451     * @return the number of matched messages (messages targeted for the
452     *         subscription but not yet able to be dispatched due to the
453     *         prefetch buffer being full).
454     */
455    public int matched() {
456        synchronized (matchedListMutex) {
457            return matched.size();
458        }
459    }
460
461    /**
462     * Sets the maximum number of pending messages that can be matched against
463     * this consumer before old messages are discarded.
464     */
465    public void setMaximumPendingMessages(int maximumPendingMessages) {
466        this.maximumPendingMessages = maximumPendingMessages;
467    }
468
469    public MessageEvictionStrategy getMessageEvictionStrategy() {
470        return messageEvictionStrategy;
471    }
472
473    /**
474     * Sets the eviction strategy used to decide which message to evict when the
475     * slow consumer needs to discard messages
476     */
477    public void setMessageEvictionStrategy(MessageEvictionStrategy messageEvictionStrategy) {
478        this.messageEvictionStrategy = messageEvictionStrategy;
479    }
480
481    public int getMaxProducersToAudit() {
482        return maxProducersToAudit;
483    }
484
485    public synchronized void setMaxProducersToAudit(int maxProducersToAudit) {
486        this.maxProducersToAudit = maxProducersToAudit;
487        if (audit != null) {
488            audit.setMaximumNumberOfProducersToTrack(maxProducersToAudit);
489        }
490    }
491
492    public int getMaxAuditDepth() {
493        return maxAuditDepth;
494    }
495
496    public synchronized void setMaxAuditDepth(int maxAuditDepth) {
497        this.maxAuditDepth = maxAuditDepth;
498        if (audit != null) {
499            audit.setAuditDepth(maxAuditDepth);
500        }
501    }
502
503    public boolean isEnableAudit() {
504        return enableAudit;
505    }
506
507    public synchronized void setEnableAudit(boolean enableAudit) {
508        this.enableAudit = enableAudit;
509        if (enableAudit && audit == null) {
510            audit = new ActiveMQMessageAudit(maxAuditDepth,maxProducersToAudit);
511        }
512    }
513
514    // Implementation methods
515    // -------------------------------------------------------------------------
516    @Override
517    public boolean isFull() {
518        return getDispatchedQueueSize() >= info.getPrefetchSize();
519    }
520
521    @Override
522    public int getInFlightSize() {
523        return getDispatchedQueueSize();
524    }
525
526    /**
527     * @return true when 60% or more room is left for dispatching messages
528     */
529    @Override
530    public boolean isLowWaterMark() {
531        return getDispatchedQueueSize() <= (info.getPrefetchSize() * .4);
532    }
533
534    /**
535     * @return true when 10% or less room is left for dispatching messages
536     */
537    @Override
538    public boolean isHighWaterMark() {
539        return getDispatchedQueueSize() >= (info.getPrefetchSize() * .9);
540    }
541
542    /**
543     * @param memoryUsageHighWaterMark the memoryUsageHighWaterMark to set
544     */
545    public void setMemoryUsageHighWaterMark(int memoryUsageHighWaterMark) {
546        this.memoryUsageHighWaterMark = memoryUsageHighWaterMark;
547    }
548
549    /**
550     * @return the memoryUsageHighWaterMark
551     */
552    public int getMemoryUsageHighWaterMark() {
553        return this.memoryUsageHighWaterMark;
554    }
555
556    /**
557     * @return the usageManager
558     */
559    public SystemUsage getUsageManager() {
560        return this.usageManager;
561    }
562
563    /**
564     * @return the matched
565     */
566    public PendingMessageCursor getMatched() {
567        return this.matched;
568    }
569
570    /**
571     * @param matched the matched to set
572     */
573    public void setMatched(PendingMessageCursor matched) {
574        this.matched = matched;
575    }
576
577    /**
578     * inform the MessageConsumer on the client to change it's prefetch
579     *
580     * @param newPrefetch
581     */
582    @Override
583    public void updateConsumerPrefetch(int newPrefetch) {
584        if (context != null && context.getConnection() != null && context.getConnection().isManageable()) {
585            ConsumerControl cc = new ConsumerControl();
586            cc.setConsumerId(info.getConsumerId());
587            cc.setPrefetch(newPrefetch);
588            context.getConnection().dispatchAsync(cc);
589        }
590    }
591
592    private void dispatchMatched() throws IOException {
593        synchronized (matchedListMutex) {
594            if (!matched.isEmpty() && !isFull()) {
595                try {
596                    matched.reset();
597
598                    while (matched.hasNext() && !isFull()) {
599                        MessageReference message = matched.next();
600                        message.decrementReferenceCount();
601                        matched.remove();
602                        // Message may have been sitting in the matched list a while
603                        // waiting for the consumer to ak the message.
604                        if (message.isExpired()) {
605                            discard(message);
606                            continue; // just drop it.
607                        }
608                        dispatch(message);
609                    }
610                } finally {
611                    matched.release();
612                }
613            }
614        }
615    }
616
617    private void dispatch(final MessageReference node) throws IOException {
618        Message message = node != null ? node.getMessage() : null;
619        if (node != null) {
620            node.incrementReferenceCount();
621        }
622        // Make sure we can dispatch a message.
623        MessageDispatch md = new MessageDispatch();
624        md.setMessage(message);
625        md.setConsumerId(info.getConsumerId());
626        if (node != null) {
627            md.setDestination(((Destination)node.getRegionDestination()).getActiveMQDestination());
628            synchronized(dispatchLock) {
629                getSubscriptionStatistics().getDispatched().increment();
630                dispatched.add(node);
631                getSubscriptionStatistics().getInflightMessageSize().addSize(node.getSize());
632            }
633
634            // Keep track if this subscription is receiving messages from a single destination.
635            if (singleDestination) {
636                if (destination == null) {
637                    destination = (Destination)node.getRegionDestination();
638                } else {
639                    if (destination != node.getRegionDestination()) {
640                        singleDestination = false;
641                    }
642                }
643            }
644        }
645        if (info.isDispatchAsync()) {
646            if (node != null) {
647                md.setTransmitCallback(new TransmitCallback() {
648
649                    @Override
650                    public void onSuccess() {
651                        Destination regionDestination = (Destination) node.getRegionDestination();
652                        regionDestination.getDestinationStatistics().getDispatched().increment();
653                        regionDestination.getDestinationStatistics().getInflight().increment();
654                        node.decrementReferenceCount();
655                    }
656
657                    @Override
658                    public void onFailure() {
659                        Destination regionDestination = (Destination) node.getRegionDestination();
660                        regionDestination.getDestinationStatistics().getDispatched().increment();
661                        regionDestination.getDestinationStatistics().getInflight().increment();
662                        node.decrementReferenceCount();
663                    }
664                });
665            }
666            context.getConnection().dispatchAsync(md);
667        } else {
668            context.getConnection().dispatchSync(md);
669            if (node != null) {
670                Destination regionDestination = (Destination) node.getRegionDestination();
671                regionDestination.getDestinationStatistics().getDispatched().increment();
672                regionDestination.getDestinationStatistics().getInflight().increment();
673                node.decrementReferenceCount();
674            }
675        }
676    }
677
678    private void discard(MessageReference message) {
679        discarding = true;
680        try {
681            message.decrementReferenceCount();
682            matched.remove(message);
683            discarded++;
684            if (destination != null) {
685                destination.getDestinationStatistics().getDequeues().increment();
686            }
687            LOG.debug("{}, discarding message {}", this, message);
688            Destination dest = (Destination) message.getRegionDestination();
689            if (dest != null) {
690                dest.messageDiscarded(getContext(), this, message);
691            }
692            broker.getRoot().sendToDeadLetterQueue(getContext(), message, this, new Throwable("TopicSubDiscard. ID:" + info.getConsumerId()));
693        } finally {
694            discarding = false;
695        }
696    }
697
698    @Override
699    public String toString() {
700        return "TopicSubscription:" + " consumer=" + info.getConsumerId() + ", destinations=" + destinations.size() + ", dispatched=" + getDispatchedQueueSize() + ", delivered="
701                + getDequeueCounter() + ", matched=" + matched() + ", discarded=" + discarded();
702    }
703
704    @Override
705    public void destroy() {
706        this.active=false;
707        synchronized (matchedListMutex) {
708            try {
709                matched.destroy();
710            } catch (Exception e) {
711                LOG.warn("Failed to destroy cursor", e);
712            }
713        }
714        setSlowConsumer(false);
715        synchronized(dispatchLock) {
716            dispatched.clear();
717        }
718    }
719
720    @Override
721    public int getPrefetchSize() {
722        return info.getPrefetchSize();
723    }
724
725    @Override
726    public void setPrefetchSize(int newSize) {
727        info.setPrefetchSize(newSize);
728        try {
729            dispatchMatched();
730        } catch(Exception e) {
731            LOG.trace("Caught exception on dispatch after prefetch size change.");
732        }
733    }
734}