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.util.HashSet; 020import java.util.Iterator; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024import java.util.Timer; 025import java.util.TimerTask; 026import java.util.concurrent.ConcurrentHashMap; 027 028import javax.jms.InvalidDestinationException; 029import javax.jms.JMSException; 030 031import org.apache.activemq.advisory.AdvisorySupport; 032import org.apache.activemq.broker.ConnectionContext; 033import org.apache.activemq.broker.region.policy.PolicyEntry; 034import org.apache.activemq.command.ActiveMQDestination; 035import org.apache.activemq.command.ConnectionId; 036import org.apache.activemq.command.ConsumerId; 037import org.apache.activemq.command.ConsumerInfo; 038import org.apache.activemq.command.RemoveSubscriptionInfo; 039import org.apache.activemq.command.SessionId; 040import org.apache.activemq.command.SubscriptionInfo; 041import org.apache.activemq.store.TopicMessageStore; 042import org.apache.activemq.thread.TaskRunnerFactory; 043import org.apache.activemq.usage.SystemUsage; 044import org.apache.activemq.util.LongSequenceGenerator; 045import org.apache.activemq.util.SubscriptionKey; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048 049/** 050 * 051 */ 052public class TopicRegion extends AbstractRegion { 053 private static final Logger LOG = LoggerFactory.getLogger(TopicRegion.class); 054 protected final ConcurrentHashMap<SubscriptionKey, DurableTopicSubscription> durableSubscriptions = new ConcurrentHashMap<SubscriptionKey, DurableTopicSubscription>(); 055 private final LongSequenceGenerator recoveredDurableSubIdGenerator = new LongSequenceGenerator(); 056 private final SessionId recoveredDurableSubSessionId = new SessionId(new ConnectionId("OFFLINE"), recoveredDurableSubIdGenerator.getNextSequenceId()); 057 private boolean keepDurableSubsActive; 058 059 private Timer cleanupTimer; 060 private TimerTask cleanupTask; 061 062 public TopicRegion(RegionBroker broker, DestinationStatistics destinationStatistics, SystemUsage memoryManager, TaskRunnerFactory taskRunnerFactory, 063 DestinationFactory destinationFactory) { 064 super(broker, destinationStatistics, memoryManager, taskRunnerFactory, destinationFactory); 065 if (broker.getBrokerService().getOfflineDurableSubscriberTaskSchedule() != -1 && broker.getBrokerService().getOfflineDurableSubscriberTimeout() != -1) { 066 this.cleanupTimer = new Timer("ActiveMQ Durable Subscriber Cleanup Timer", true); 067 this.cleanupTask = new TimerTask() { 068 @Override 069 public void run() { 070 doCleanup(); 071 } 072 }; 073 this.cleanupTimer.schedule(cleanupTask, broker.getBrokerService().getOfflineDurableSubscriberTaskSchedule(), broker.getBrokerService().getOfflineDurableSubscriberTaskSchedule()); 074 } 075 } 076 077 @Override 078 public void stop() throws Exception { 079 super.stop(); 080 if (cleanupTimer != null) { 081 cleanupTimer.cancel(); 082 } 083 } 084 085 public void doCleanup() { 086 long now = System.currentTimeMillis(); 087 for (Map.Entry<SubscriptionKey, DurableTopicSubscription> entry : durableSubscriptions.entrySet()) { 088 DurableTopicSubscription sub = entry.getValue(); 089 if (!sub.isActive()) { 090 long offline = sub.getOfflineTimestamp(); 091 if (offline != -1 && now - offline >= broker.getBrokerService().getOfflineDurableSubscriberTimeout()) { 092 LOG.info("Destroying durable subscriber due to inactivity: {}", sub); 093 try { 094 RemoveSubscriptionInfo info = new RemoveSubscriptionInfo(); 095 info.setClientId(entry.getKey().getClientId()); 096 info.setSubscriptionName(entry.getKey().getSubscriptionName()); 097 ConnectionContext context = new ConnectionContext(); 098 context.setBroker(broker); 099 context.setClientId(entry.getKey().getClientId()); 100 removeSubscription(context, info); 101 } catch (Exception e) { 102 LOG.error("Failed to remove inactive durable subscriber", e); 103 } 104 } 105 } 106 } 107 } 108 109 @Override 110 public Subscription addConsumer(ConnectionContext context, ConsumerInfo info) throws Exception { 111 if (info.isDurable()) { 112 ActiveMQDestination destination = info.getDestination(); 113 if (!destination.isPattern()) { 114 // Make sure the destination is created. 115 lookup(context, destination,true); 116 } 117 String clientId = context.getClientId(); 118 String subscriptionName = info.getSubscriptionName(); 119 SubscriptionKey key = new SubscriptionKey(clientId, subscriptionName); 120 DurableTopicSubscription sub = durableSubscriptions.get(key); 121 if (sub != null) { 122 // throw this exception only if link stealing is off 123 if (!context.isAllowLinkStealing() && sub.isActive()) { 124 throw new JMSException("Durable consumer is in use for client: " + clientId + 125 " and subscriptionName: " + subscriptionName); 126 } 127 // Has the selector changed?? 128 if (hasDurableSubChanged(info, sub.getConsumerInfo())) { 129 // Remove the consumer first then add it. 130 durableSubscriptions.remove(key); 131 destinationsLock.readLock().lock(); 132 try { 133 for (Destination dest : destinations.values()) { 134 //Account for virtual destinations 135 if (dest instanceof Topic){ 136 Topic topic = (Topic)dest; 137 topic.deleteSubscription(context, key); 138 } 139 } 140 } finally { 141 destinationsLock.readLock().unlock(); 142 } 143 super.removeConsumer(context, sub.getConsumerInfo()); 144 super.addConsumer(context, info); 145 sub = durableSubscriptions.get(key); 146 } else { 147 // Change the consumer id key of the durable sub. 148 if (sub.getConsumerInfo().getConsumerId() != null) { 149 subscriptions.remove(sub.getConsumerInfo().getConsumerId()); 150 } 151 // set the info and context to the new ones. 152 // this is set in the activate() call below, but 153 // that call is a NOP if it is already active. 154 // hence need to set here and deactivate it first 155 if ((sub.context != context) || (sub.info != info)) { 156 sub.info = info; 157 sub.context = context; 158 sub.deactivate(keepDurableSubsActive, info.getLastDeliveredSequenceId()); 159 } 160 subscriptions.put(info.getConsumerId(), sub); 161 } 162 } else { 163 super.addConsumer(context, info); 164 sub = durableSubscriptions.get(key); 165 if (sub == null) { 166 throw new JMSException("Cannot use the same consumerId: " + info.getConsumerId() + 167 " for two different durable subscriptions clientID: " + key.getClientId() + 168 " subscriberName: " + key.getSubscriptionName()); 169 } 170 } 171 sub.activate(usageManager, context, info, broker); 172 return sub; 173 } else { 174 return super.addConsumer(context, info); 175 } 176 } 177 178 @Override 179 public void removeConsumer(ConnectionContext context, ConsumerInfo info) throws Exception { 180 if (info.isDurable()) { 181 SubscriptionKey key = new SubscriptionKey(context.getClientId(), info.getSubscriptionName()); 182 DurableTopicSubscription sub = durableSubscriptions.get(key); 183 if (sub != null) { 184 // deactivate only if given context is same 185 // as what is in the sub. otherwise, during linksteal 186 // sub will get new context, but will be removed here 187 if (sub.getContext() == context) 188 sub.deactivate(keepDurableSubsActive, info.getLastDeliveredSequenceId()); 189 } 190 } else { 191 super.removeConsumer(context, info); 192 } 193 } 194 195 @Override 196 public void removeSubscription(ConnectionContext context, RemoveSubscriptionInfo info) throws Exception { 197 SubscriptionKey key = new SubscriptionKey(info.getClientId(), info.getSubscriptionName()); 198 DurableTopicSubscription sub = durableSubscriptions.get(key); 199 if (sub == null) { 200 throw new InvalidDestinationException("No durable subscription exists for: " + info.getSubscriptionName()); 201 } 202 if (sub.isActive()) { 203 throw new JMSException("Durable consumer is in use"); 204 } else { 205 durableSubscriptions.remove(key); 206 } 207 208 destinationsLock.readLock().lock(); 209 try { 210 for (Destination dest : destinations.values()) { 211 if (dest instanceof Topic){ 212 Topic topic = (Topic)dest; 213 topic.deleteSubscription(context, key); 214 } else if (dest instanceof DestinationFilter) { 215 DestinationFilter filter = (DestinationFilter) dest; 216 filter.deleteSubscription(context, key); 217 } 218 } 219 } finally { 220 destinationsLock.readLock().unlock(); 221 } 222 223 if (subscriptions.get(sub.getConsumerInfo().getConsumerId()) != null) { 224 super.removeConsumer(context, sub.getConsumerInfo()); 225 } else { 226 // try destroying inactive subscriptions 227 destroySubscription(sub); 228 } 229 } 230 231 @Override 232 public String toString() { 233 return "TopicRegion: destinations=" + destinations.size() + ", subscriptions=" + subscriptions.size() + ", memory=" + usageManager.getMemoryUsage().getPercentUsage() + "%"; 234 } 235 236 @Override 237 protected List<Subscription> addSubscriptionsForDestination(ConnectionContext context, Destination dest) throws Exception { 238 List<Subscription> rc = super.addSubscriptionsForDestination(context, dest); 239 Set<Subscription> dupChecker = new HashSet<Subscription>(rc); 240 241 TopicMessageStore store = (TopicMessageStore)dest.getMessageStore(); 242 // Eagerly recover the durable subscriptions 243 if (store != null) { 244 SubscriptionInfo[] infos = store.getAllSubscriptions(); 245 for (int i = 0; i < infos.length; i++) { 246 247 SubscriptionInfo info = infos[i]; 248 LOG.debug("Restoring durable subscription: {}", info); 249 SubscriptionKey key = new SubscriptionKey(info); 250 251 // A single durable sub may be subscribing to multiple topics. 252 // so it might exist already. 253 DurableTopicSubscription sub = durableSubscriptions.get(key); 254 ConsumerInfo consumerInfo = createInactiveConsumerInfo(info); 255 if (sub == null) { 256 ConnectionContext c = new ConnectionContext(); 257 c.setBroker(context.getBroker()); 258 c.setClientId(key.getClientId()); 259 c.setConnectionId(consumerInfo.getConsumerId().getParentId().getParentId()); 260 sub = (DurableTopicSubscription)createSubscription(c, consumerInfo); 261 sub.setOfflineTimestamp(System.currentTimeMillis()); 262 } 263 264 if (dupChecker.contains(sub)) { 265 continue; 266 } 267 268 dupChecker.add(sub); 269 rc.add(sub); 270 dest.addSubscription(context, sub); 271 } 272 273 // Now perhaps there other durable subscriptions (via wild card) 274 // that would match this destination.. 275 durableSubscriptions.values(); 276 for (DurableTopicSubscription sub : durableSubscriptions.values()) { 277 // Skip over subscriptions that we already added.. 278 if (dupChecker.contains(sub)) { 279 continue; 280 } 281 282 if (sub.matches(dest.getActiveMQDestination())) { 283 rc.add(sub); 284 dest.addSubscription(context, sub); 285 } 286 } 287 } 288 return rc; 289 } 290 291 public ConsumerInfo createInactiveConsumerInfo(SubscriptionInfo info) { 292 ConsumerInfo rc = new ConsumerInfo(); 293 rc.setSelector(info.getSelector()); 294 rc.setSubscriptionName(info.getSubscriptionName()); 295 rc.setDestination(info.getSubscribedDestination()); 296 rc.setConsumerId(createConsumerId()); 297 return rc; 298 } 299 300 private ConsumerId createConsumerId() { 301 return new ConsumerId(recoveredDurableSubSessionId, recoveredDurableSubIdGenerator.getNextSequenceId()); 302 } 303 304 protected void configureTopic(Topic topic, ActiveMQDestination destination) { 305 if (broker.getDestinationPolicy() != null) { 306 PolicyEntry entry = broker.getDestinationPolicy().getEntryFor(destination); 307 if (entry != null) { 308 entry.configure(broker,topic); 309 } 310 } 311 } 312 313 @Override 314 protected Subscription createSubscription(ConnectionContext context, ConsumerInfo info) throws JMSException { 315 ActiveMQDestination destination = info.getDestination(); 316 317 if (info.isDurable()) { 318 if (AdvisorySupport.isAdvisoryTopic(info.getDestination())) { 319 throw new JMSException("Cannot create a durable subscription for an advisory Topic"); 320 } 321 SubscriptionKey key = new SubscriptionKey(context.getClientId(), info.getSubscriptionName()); 322 DurableTopicSubscription sub = durableSubscriptions.get(key); 323 324 if (sub == null) { 325 326 sub = new DurableTopicSubscription(broker, usageManager, context, info, keepDurableSubsActive); 327 328 if (destination != null && broker.getDestinationPolicy() != null) { 329 PolicyEntry entry = broker.getDestinationPolicy().getEntryFor(destination); 330 if (entry != null) { 331 entry.configure(broker, usageManager, sub); 332 } 333 } 334 durableSubscriptions.put(key, sub); 335 } else { 336 throw new JMSException("That durable subscription is already active."); 337 } 338 return sub; 339 } 340 try { 341 TopicSubscription answer = new TopicSubscription(broker, context, info, usageManager); 342 // lets configure the subscription depending on the destination 343 if (destination != null && broker.getDestinationPolicy() != null) { 344 PolicyEntry entry = broker.getDestinationPolicy().getEntryFor(destination); 345 if (entry != null) { 346 entry.configure(broker, usageManager, answer); 347 } 348 } 349 answer.init(); 350 return answer; 351 } catch (Exception e) { 352 LOG.error("Failed to create TopicSubscription ", e); 353 JMSException jmsEx = new JMSException("Couldn't create TopicSubscription"); 354 jmsEx.setLinkedException(e); 355 throw jmsEx; 356 } 357 } 358 359 private boolean hasDurableSubChanged(ConsumerInfo info1, ConsumerInfo info2) { 360 if (info1.getSelector() != null ^ info2.getSelector() != null) { 361 return true; 362 } 363 if (info1.getSelector() != null && !info1.getSelector().equals(info2.getSelector())) { 364 return true; 365 } 366 return !info1.getDestination().equals(info2.getDestination()); 367 } 368 369 @Override 370 protected Set<ActiveMQDestination> getInactiveDestinations() { 371 Set<ActiveMQDestination> inactiveDestinations = super.getInactiveDestinations(); 372 for (Iterator<ActiveMQDestination> iter = inactiveDestinations.iterator(); iter.hasNext();) { 373 ActiveMQDestination dest = iter.next(); 374 if (!dest.isTopic()) { 375 iter.remove(); 376 } 377 } 378 return inactiveDestinations; 379 } 380 381 public boolean isKeepDurableSubsActive() { 382 return keepDurableSubsActive; 383 } 384 385 public void setKeepDurableSubsActive(boolean keepDurableSubsActive) { 386 this.keepDurableSubsActive = keepDurableSubsActive; 387 } 388 389 public boolean durableSubscriptionExists(SubscriptionKey key) { 390 return this.durableSubscriptions.containsKey(key); 391 } 392 393 public DurableTopicSubscription getDurableSubscription(SubscriptionKey key) { 394 return durableSubscriptions.get(key); 395 } 396}