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