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