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.Collections;
022import java.util.HashSet;
023import java.util.List;
024import java.util.concurrent.ConcurrentHashMap;
025import java.util.concurrent.ConcurrentMap;
026import java.util.concurrent.atomic.AtomicBoolean;
027import java.util.concurrent.atomic.AtomicLong;
028
029import javax.jms.InvalidSelectorException;
030import javax.jms.JMSException;
031
032import org.apache.activemq.broker.Broker;
033import org.apache.activemq.broker.ConnectionContext;
034import org.apache.activemq.broker.region.cursors.AbstractPendingMessageCursor;
035import org.apache.activemq.broker.region.cursors.PendingMessageCursor;
036import org.apache.activemq.broker.region.cursors.StoreDurableSubscriberCursor;
037import org.apache.activemq.broker.region.policy.PolicyEntry;
038import org.apache.activemq.command.ActiveMQDestination;
039import org.apache.activemq.command.ConsumerInfo;
040import org.apache.activemq.command.Message;
041import org.apache.activemq.command.MessageAck;
042import org.apache.activemq.command.MessageDispatch;
043import org.apache.activemq.command.MessageId;
044import org.apache.activemq.command.RemoveInfo;
045import org.apache.activemq.store.TopicMessageStore;
046import org.apache.activemq.transaction.Synchronization;
047import org.apache.activemq.usage.SystemUsage;
048import org.apache.activemq.usage.Usage;
049import org.apache.activemq.usage.UsageListener;
050import org.apache.activemq.util.SubscriptionKey;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054public class DurableTopicSubscription extends PrefetchSubscription implements UsageListener {
055
056    private static final Logger LOG = LoggerFactory.getLogger(DurableTopicSubscription.class);
057    private final ConcurrentMap<MessageId, Integer> redeliveredMessages = new ConcurrentHashMap<MessageId, Integer>();
058    private final ConcurrentMap<ActiveMQDestination, Destination> durableDestinations = new ConcurrentHashMap<ActiveMQDestination, Destination>();
059    private final SubscriptionKey subscriptionKey;
060    private boolean keepDurableSubsActive;
061    private final AtomicBoolean active = new AtomicBoolean();
062    private final AtomicLong offlineTimestamp = new AtomicLong(-1);
063    private final HashSet<MessageId> ackedAndPrepared = new HashSet<MessageId>();
064
065    public DurableTopicSubscription(Broker broker, SystemUsage usageManager, ConnectionContext context, ConsumerInfo info, boolean keepDurableSubsActive)
066            throws JMSException {
067        super(broker, usageManager, context, info);
068        this.pending = new StoreDurableSubscriberCursor(broker, context.getClientId(), info.getSubscriptionName(), info.getPrefetchSize(), this);
069        this.pending.setSystemUsage(usageManager);
070        this.pending.setMemoryUsageHighWaterMark(getCursorMemoryHighWaterMark());
071        this.keepDurableSubsActive = keepDurableSubsActive;
072        subscriptionKey = new SubscriptionKey(context.getClientId(), info.getSubscriptionName());
073    }
074
075    public final boolean isActive() {
076        return active.get();
077    }
078
079    public final long getOfflineTimestamp() {
080        return offlineTimestamp.get();
081    }
082
083    public void setOfflineTimestamp(long timestamp) {
084        offlineTimestamp.set(timestamp);
085    }
086
087    @Override
088    public boolean isFull() {
089        return !active.get() || super.isFull();
090    }
091
092    @Override
093    public void gc() {
094    }
095
096    /**
097     * store will have a pending ack for all durables, irrespective of the
098     * selector so we need to ack if node is un-matched
099     */
100    @Override
101    public void unmatched(MessageReference node) throws IOException {
102        MessageAck ack = new MessageAck();
103        ack.setAckType(MessageAck.UNMATCHED_ACK_TYPE);
104        ack.setMessageID(node.getMessageId());
105        Destination regionDestination = (Destination) node.getRegionDestination();
106        regionDestination.acknowledge(this.getContext(), this, ack, node);
107    }
108
109    @Override
110    protected void setPendingBatchSize(PendingMessageCursor pending, int numberToDispatch) {
111        // statically configured via maxPageSize
112    }
113
114    @Override
115    public void add(ConnectionContext context, Destination destination) throws Exception {
116        if (!destinations.contains(destination)) {
117            super.add(context, destination);
118        }
119        // do it just once per destination
120        if (durableDestinations.containsKey(destination.getActiveMQDestination())) {
121            return;
122        }
123        durableDestinations.put(destination.getActiveMQDestination(), destination);
124
125        if (active.get() || keepDurableSubsActive) {
126            Topic topic = (Topic) destination;
127            topic.activate(context, this);
128            getSubscriptionStatistics().getEnqueues().add(pending.size());
129        } else if (destination.getMessageStore() != null) {
130            TopicMessageStore store = (TopicMessageStore) destination.getMessageStore();
131            try {
132                getSubscriptionStatistics().getEnqueues().add(store.getMessageCount(subscriptionKey.getClientId(), subscriptionKey.getSubscriptionName()));
133            } catch (IOException e) {
134                JMSException jmsEx = new JMSException("Failed to retrieve enqueueCount from store " + e);
135                jmsEx.setLinkedException(e);
136                throw jmsEx;
137            }
138        }
139        dispatchPending();
140    }
141
142    // used by RetaineMessageSubscriptionRecoveryPolicy
143    public boolean isEmpty(Topic topic) {
144        return pending.isEmpty(topic);
145    }
146
147    public void activate(SystemUsage memoryManager, ConnectionContext context, ConsumerInfo info, RegionBroker regionBroker) throws Exception {
148        if (!active.get()) {
149            this.context = context;
150            this.info = info;
151
152            LOG.debug("Activating {}", this);
153            if (!keepDurableSubsActive) {
154                for (Destination destination : durableDestinations.values()) {
155                    Topic topic = (Topic) destination;
156                    add(context, topic);
157                    topic.activate(context, this);
158                }
159
160                // On Activation we should update the configuration based on our new consumer info.
161                ActiveMQDestination dest = this.info.getDestination();
162                if (dest != null && regionBroker.getDestinationPolicy() != null) {
163                    PolicyEntry entry = regionBroker.getDestinationPolicy().getEntryFor(dest);
164                    if (entry != null) {
165                        entry.configure(broker, usageManager, this);
166                    }
167                }
168            }
169
170            synchronized (pendingLock) {
171                if (!((AbstractPendingMessageCursor) pending).isStarted() || !keepDurableSubsActive) {
172                    pending.setSystemUsage(memoryManager);
173                    pending.setMemoryUsageHighWaterMark(getCursorMemoryHighWaterMark());
174                    pending.setMaxAuditDepth(getMaxAuditDepth());
175                    pending.setMaxProducersToAudit(getMaxProducersToAudit());
176                    pending.start();
177                }
178                // use recovery policy every time sub is activated for retroactive topics and consumers
179                for (Destination destination : durableDestinations.values()) {
180                    Topic topic = (Topic) destination;
181                    if (topic.isAlwaysRetroactive() || info.isRetroactive()) {
182                        topic.recoverRetroactiveMessages(context, this);
183                    }
184                }
185            }
186            this.active.set(true);
187            this.offlineTimestamp.set(-1);
188            dispatchPending();
189            this.usageManager.getMemoryUsage().addUsageListener(this);
190        }
191    }
192
193    public void deactivate(boolean keepDurableSubsActive, long lastDeliveredSequenceId) throws Exception {
194        LOG.debug("Deactivating keepActive={}, {}", keepDurableSubsActive, this);
195        active.set(false);
196        this.keepDurableSubsActive = keepDurableSubsActive;
197        offlineTimestamp.set(System.currentTimeMillis());
198        usageManager.getMemoryUsage().removeUsageListener(this);
199
200        ArrayList<Topic> topicsToDeactivate = new ArrayList<Topic>();
201        List<MessageReference> savedDispateched = null;
202
203        synchronized (pendingLock) {
204            if (!keepDurableSubsActive) {
205                pending.stop();
206            }
207
208            synchronized (dispatchLock) {
209                for (Destination destination : durableDestinations.values()) {
210                    Topic topic = (Topic) destination;
211                    if (!keepDurableSubsActive) {
212                        topicsToDeactivate.add(topic);
213                    } else {
214                        topic.getDestinationStatistics().getInflight().subtract(dispatched.size());
215                    }
216                }
217
218                // Before we add these back to pending they need to be in producer order not
219                // dispatch order so we can add them to the front of the pending list.
220                Collections.reverse(dispatched);
221
222                for (final MessageReference node : dispatched) {
223                    // Mark the dispatched messages as redelivered for next time.
224                    if (lastDeliveredSequenceId == RemoveInfo.LAST_DELIVERED_UNKNOWN || lastDeliveredSequenceId == 0 ||
225                            (lastDeliveredSequenceId > 0 && node.getMessageId().getBrokerSequenceId() <= lastDeliveredSequenceId)) {
226                        Integer count = redeliveredMessages.get(node.getMessageId());
227                        if (count != null) {
228                            redeliveredMessages.put(node.getMessageId(), Integer.valueOf(count.intValue() + 1));
229                        } else {
230                            redeliveredMessages.put(node.getMessageId(), Integer.valueOf(1));
231                        }
232                    }
233                    if (keepDurableSubsActive && pending.isTransient()) {
234                        pending.addMessageFirst(node);
235                        pending.rollback(node.getMessageId());
236                    }
237                    // createMessageDispatch increments on remove from pending for dispatch
238                    node.decrementReferenceCount();
239                }
240
241                if (!topicsToDeactivate.isEmpty()) {
242                    savedDispateched = new ArrayList<MessageReference>(dispatched);
243                }
244                dispatched.clear();
245                getSubscriptionStatistics().getInflightMessageSize().reset();
246            }
247            if (!keepDurableSubsActive && pending.isTransient()) {
248                try {
249                    pending.reset();
250                    while (pending.hasNext()) {
251                        MessageReference node = pending.next();
252                        node.decrementReferenceCount();
253                        pending.remove();
254                    }
255                } finally {
256                    pending.release();
257                }
258            }
259        }
260        for(Topic topic: topicsToDeactivate) {
261            topic.deactivate(context, this, savedDispateched);
262        }
263        prefetchExtension.set(0);
264    }
265
266    @Override
267    protected MessageDispatch createMessageDispatch(MessageReference node, Message message) {
268        MessageDispatch md = super.createMessageDispatch(node, message);
269        if (node != QueueMessageReference.NULL_MESSAGE) {
270            node.incrementReferenceCount();
271            Integer count = redeliveredMessages.get(node.getMessageId());
272            if (count != null) {
273                md.setRedeliveryCounter(count.intValue());
274            }
275        }
276        return md;
277    }
278
279    @Override
280    public void add(MessageReference node) throws Exception {
281        if (!active.get() && !keepDurableSubsActive) {
282            return;
283        }
284        super.add(node);
285    }
286
287    @Override
288    public void dispatchPending() throws IOException {
289        if (isActive()) {
290            super.dispatchPending();
291        }
292    }
293
294    public void removePending(MessageReference node) throws IOException {
295        pending.remove(node);
296    }
297
298    @Override
299    protected void doAddRecoveredMessage(MessageReference message) throws Exception {
300        synchronized (pending) {
301            pending.addRecoveredMessage(message);
302        }
303    }
304
305    @Override
306    public int getPendingQueueSize() {
307        if (active.get() || keepDurableSubsActive) {
308            return super.getPendingQueueSize();
309        }
310        // TODO: need to get from store
311        return 0;
312    }
313
314    @Override
315    public void setSelector(String selector) throws InvalidSelectorException {
316        if (active.get()) {
317            throw new UnsupportedOperationException("You cannot dynamically change the selector for durable topic subscriptions");
318        } else {
319            super.setSelector(getSelector());
320        }
321    }
322
323    @Override
324    protected boolean canDispatch(MessageReference node) {
325        return true;  // let them go, our dispatchPending gates the active / inactive state.
326    }
327
328    @Override
329    protected boolean trackedInPendingTransaction(MessageReference node) {
330        return !ackedAndPrepared.isEmpty() && ackedAndPrepared.contains(node.getMessageId());
331    }
332
333    @Override
334    protected void acknowledge(ConnectionContext context, MessageAck ack, final MessageReference node) throws IOException {
335        this.setTimeOfLastMessageAck(System.currentTimeMillis());
336        Destination regionDestination = (Destination) node.getRegionDestination();
337        regionDestination.acknowledge(context, this, ack, node);
338        redeliveredMessages.remove(node.getMessageId());
339        node.decrementReferenceCount();
340        if (context.isInTransaction() && context.getTransaction().getTransactionId().isXATransaction()) {
341            context.getTransaction().addSynchronization(new Synchronization() {
342
343                @Override
344                public void beforeCommit() throws Exception {
345                    // post xa prepare call
346                    synchronized (pendingLock) {
347                        ackedAndPrepared.add(node.getMessageId());
348                    }
349                }
350
351                @Override
352                public void afterCommit() throws Exception {
353                    synchronized (pendingLock) {
354                        // may be in the cursor post activate/load from the store
355                        pending.remove(node);
356                        ackedAndPrepared.remove(node.getMessageId());
357                    }
358                }
359
360                @Override
361                public void afterRollback() throws Exception {
362                    synchronized (pendingLock) {
363                        ackedAndPrepared.remove(node.getMessageId());
364                    }
365                    dispatchPending();
366                }
367            });
368        }
369        ((Destination)node.getRegionDestination()).getDestinationStatistics().getDequeues().increment();
370        if (info.isNetworkSubscription()) {
371            ((Destination)node.getRegionDestination()).getDestinationStatistics().getForwards().add(ack.getMessageCount());
372        }
373    }
374
375    @Override
376    public synchronized String toString() {
377        return "DurableTopicSubscription-" + getSubscriptionKey() + ", id=" + info.getConsumerId() + ", active=" + isActive() + ", destinations="
378                + durableDestinations.size() + ", total=" + getSubscriptionStatistics().getEnqueues().getCount() + ", pending=" + getPendingQueueSize() + ", dispatched=" + getSubscriptionStatistics().getDispatched().getCount()
379                + ", inflight=" + dispatched.size() + ", prefetchExtension=" + getPrefetchExtension();
380    }
381
382    public SubscriptionKey getSubscriptionKey() {
383        return subscriptionKey;
384    }
385
386    /**
387     * Release any references that we are holding.
388     */
389    @Override
390    public void destroy() {
391        synchronized (pendingLock) {
392            try {
393                pending.reset();
394                while (pending.hasNext()) {
395                    MessageReference node = pending.next();
396                    node.decrementReferenceCount();
397                }
398            } finally {
399                pending.release();
400                pending.clear();
401            }
402        }
403        synchronized (dispatchLock) {
404            for (MessageReference node : dispatched) {
405                node.decrementReferenceCount();
406            }
407            dispatched.clear();
408            ackedAndPrepared.clear();
409        }
410        setSlowConsumer(false);
411    }
412
413    @Override
414    public void onUsageChanged(Usage usage, int oldPercentUsage, int newPercentUsage) {
415        if (oldPercentUsage > newPercentUsage && oldPercentUsage >= 90) {
416            try {
417                dispatchPending();
418            } catch (IOException e) {
419                LOG.warn("problem calling dispatchMatched", e);
420            }
421        }
422    }
423
424    @Override
425    protected boolean isDropped(MessageReference node) {
426        return false;
427    }
428
429    public boolean isKeepDurableSubsActive() {
430        return keepDurableSubsActive;
431    }
432}