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