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