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.LinkedList; 021import java.util.concurrent.atomic.AtomicInteger; 022import java.util.concurrent.atomic.AtomicLong; 023 024import javax.jms.JMSException; 025 026import org.apache.activemq.ActiveMQMessageAudit; 027import org.apache.activemq.broker.Broker; 028import org.apache.activemq.broker.ConnectionContext; 029import org.apache.activemq.broker.region.cursors.FilePendingMessageCursor; 030import org.apache.activemq.broker.region.cursors.PendingMessageCursor; 031import org.apache.activemq.broker.region.cursors.VMPendingMessageCursor; 032import org.apache.activemq.broker.region.policy.MessageEvictionStrategy; 033import org.apache.activemq.broker.region.policy.OldestMessageEvictionStrategy; 034import org.apache.activemq.command.ConsumerControl; 035import org.apache.activemq.command.ConsumerInfo; 036import org.apache.activemq.command.Message; 037import org.apache.activemq.command.MessageAck; 038import org.apache.activemq.command.MessageDispatch; 039import org.apache.activemq.command.MessageDispatchNotification; 040import org.apache.activemq.command.MessagePull; 041import org.apache.activemq.command.Response; 042import org.apache.activemq.thread.Scheduler; 043import org.apache.activemq.transaction.Synchronization; 044import org.apache.activemq.transport.TransmitCallback; 045import org.apache.activemq.usage.SystemUsage; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048 049public class TopicSubscription extends AbstractSubscription { 050 051 private static final Logger LOG = LoggerFactory.getLogger(TopicSubscription.class); 052 private static final AtomicLong CURSOR_NAME_COUNTER = new AtomicLong(0); 053 054 protected PendingMessageCursor matched; 055 protected final SystemUsage usageManager; 056 protected AtomicLong dispatchedCounter = new AtomicLong(); 057 058 boolean singleDestination = true; 059 Destination destination; 060 private final Scheduler scheduler; 061 062 private int maximumPendingMessages = -1; 063 private MessageEvictionStrategy messageEvictionStrategy = new OldestMessageEvictionStrategy(); 064 private int discarded; 065 private final Object matchedListMutex = new Object(); 066 private final AtomicLong enqueueCounter = new AtomicLong(0); 067 private final AtomicLong dequeueCounter = new AtomicLong(0); 068 private final AtomicInteger prefetchExtension = new AtomicInteger(0); 069 private int memoryUsageHighWaterMark = 95; 070 // allow duplicate suppression in a ring network of brokers 071 protected int maxProducersToAudit = 1024; 072 protected int maxAuditDepth = 1000; 073 protected boolean enableAudit = false; 074 protected ActiveMQMessageAudit audit; 075 protected boolean active = false; 076 077 public TopicSubscription(Broker broker,ConnectionContext context, ConsumerInfo info, SystemUsage usageManager) throws Exception { 078 super(broker, context, info); 079 this.usageManager = usageManager; 080 String matchedName = "TopicSubscription:" + CURSOR_NAME_COUNTER.getAndIncrement() + "[" + info.getConsumerId().toString() + "]"; 081 if (info.getDestination().isTemporary() || broker.getTempDataStore()==null ) { 082 this.matched = new VMPendingMessageCursor(false); 083 } else { 084 this.matched = new FilePendingMessageCursor(broker,matchedName,false); 085 } 086 087 this.scheduler = broker.getScheduler(); 088 } 089 090 public void init() throws Exception { 091 this.matched.setSystemUsage(usageManager); 092 this.matched.setMemoryUsageHighWaterMark(getCursorMemoryHighWaterMark()); 093 this.matched.start(); 094 if (enableAudit) { 095 audit= new ActiveMQMessageAudit(maxAuditDepth, maxProducersToAudit); 096 } 097 this.active=true; 098 } 099 100 @Override 101 public void add(MessageReference node) throws Exception { 102 if (isDuplicate(node)) { 103 return; 104 } 105 // Lets use an indirect reference so that we can associate a unique 106 // locator /w the message. 107 node = new IndirectMessageReference(node.getMessage()); 108 enqueueCounter.incrementAndGet(); 109 synchronized (matchedListMutex) { 110 if (!isFull() && matched.isEmpty()) { 111 // if maximumPendingMessages is set we will only discard messages which 112 // have not been dispatched (i.e. we allow the prefetch buffer to be filled) 113 dispatch(node); 114 setSlowConsumer(false); 115 } else { 116 if (info.getPrefetchSize() > 1 && matched.size() > info.getPrefetchSize()) { 117 // Slow consumers should log and set their state as such. 118 if (!isSlowConsumer()) { 119 LOG.warn("{}: has twice its prefetch limit pending, without an ack; it appears to be slow", toString()); 120 setSlowConsumer(true); 121 for (Destination dest: destinations) { 122 dest.slowConsumer(getContext(), this); 123 } 124 } 125 } 126 if (maximumPendingMessages != 0) { 127 boolean warnedAboutWait = false; 128 while (active) { 129 while (matched.isFull()) { 130 if (getContext().getStopping().get()) { 131 LOG.warn("{}: stopped waiting for space in pendingMessage cursor for: {}", toString(), node.getMessageId()); 132 enqueueCounter.decrementAndGet(); 133 return; 134 } 135 if (!warnedAboutWait) { 136 LOG.info("{}: Pending message cursor [{}] is full, temp usag ({}%) or memory usage ({}%) limit reached, blocking message add() pending the release of resources.", 137 new Object[]{ 138 toString(), 139 matched, 140 matched.getSystemUsage().getTempUsage().getPercentUsage(), 141 matched.getSystemUsage().getMemoryUsage().getPercentUsage() 142 }); 143 warnedAboutWait = true; 144 } 145 matchedListMutex.wait(20); 146 } 147 // Temporary storage could be full - so just try to add the message 148 // see https://issues.apache.org/activemq/browse/AMQ-2475 149 if (matched.tryAddMessageLast(node, 10)) { 150 break; 151 } 152 } 153 if (maximumPendingMessages > 0) { 154 // calculate the high water mark from which point we 155 // will eagerly evict expired messages 156 int max = messageEvictionStrategy.getEvictExpiredMessagesHighWatermark(); 157 if (maximumPendingMessages > 0 && maximumPendingMessages < max) { 158 max = maximumPendingMessages; 159 } 160 if (!matched.isEmpty() && matched.size() > max) { 161 removeExpiredMessages(); 162 } 163 // lets discard old messages as we are a slow consumer 164 while (!matched.isEmpty() && matched.size() > maximumPendingMessages) { 165 int pageInSize = matched.size() - maximumPendingMessages; 166 // only page in a 1000 at a time - else we could blow the memory 167 pageInSize = Math.max(1000, pageInSize); 168 LinkedList<MessageReference> list = null; 169 MessageReference[] oldMessages=null; 170 synchronized(matched){ 171 list = matched.pageInList(pageInSize); 172 oldMessages = messageEvictionStrategy.evictMessages(list); 173 for (MessageReference ref : list) { 174 ref.decrementReferenceCount(); 175 } 176 } 177 int messagesToEvict = 0; 178 if (oldMessages != null){ 179 messagesToEvict = oldMessages.length; 180 for (int i = 0; i < messagesToEvict; i++) { 181 MessageReference oldMessage = oldMessages[i]; 182 discard(oldMessage); 183 } 184 } 185 // lets avoid an infinite loop if we are given a bad eviction strategy 186 // for a bad strategy lets just not evict 187 if (messagesToEvict == 0) { 188 LOG.warn("No messages to evict returned for {} from eviction strategy: {} out of {} candidates", new Object[]{ 189 destination, messageEvictionStrategy, list.size() 190 }); 191 break; 192 } 193 } 194 } 195 dispatchMatched(); 196 } 197 } 198 } 199 } 200 201 private boolean isDuplicate(MessageReference node) { 202 boolean duplicate = false; 203 if (enableAudit && audit != null) { 204 duplicate = audit.isDuplicate(node); 205 if (LOG.isDebugEnabled()) { 206 if (duplicate) { 207 LOG.debug("{}, ignoring duplicate add: {}", this, node.getMessageId()); 208 } 209 } 210 } 211 return duplicate; 212 } 213 214 /** 215 * Discard any expired messages from the matched list. Called from a 216 * synchronized block. 217 * 218 * @throws IOException 219 */ 220 protected void removeExpiredMessages() throws IOException { 221 try { 222 matched.reset(); 223 while (matched.hasNext()) { 224 MessageReference node = matched.next(); 225 node.decrementReferenceCount(); 226 if (broker.isExpired(node)) { 227 matched.remove(); 228 dispatchedCounter.incrementAndGet(); 229 node.decrementReferenceCount(); 230 ((Destination)node.getRegionDestination()).getDestinationStatistics().getExpired().increment(); 231 broker.messageExpired(getContext(), node, this); 232 break; 233 } 234 } 235 } finally { 236 matched.release(); 237 } 238 } 239 240 @Override 241 public void processMessageDispatchNotification(MessageDispatchNotification mdn) { 242 synchronized (matchedListMutex) { 243 try { 244 matched.reset(); 245 while (matched.hasNext()) { 246 MessageReference node = matched.next(); 247 node.decrementReferenceCount(); 248 if (node.getMessageId().equals(mdn.getMessageId())) { 249 matched.remove(); 250 dispatchedCounter.incrementAndGet(); 251 node.decrementReferenceCount(); 252 break; 253 } 254 } 255 } finally { 256 matched.release(); 257 } 258 } 259 } 260 261 @Override 262 public synchronized void acknowledge(final ConnectionContext context, final MessageAck ack) throws Exception { 263 super.acknowledge(context, ack); 264 265 // Handle the standard acknowledgment case. 266 if (ack.isStandardAck() || ack.isPoisonAck() || ack.isIndividualAck()) { 267 if (context.isInTransaction()) { 268 context.getTransaction().addSynchronization(new Synchronization() { 269 270 @Override 271 public void afterCommit() throws Exception { 272 synchronized (TopicSubscription.this) { 273 if (singleDestination && destination != null) { 274 destination.getDestinationStatistics().getDequeues().add(ack.getMessageCount()); 275 } 276 } 277 dequeueCounter.addAndGet(ack.getMessageCount()); 278 dispatchMatched(); 279 } 280 }); 281 } else { 282 if (singleDestination && destination != null) { 283 destination.getDestinationStatistics().getDequeues().add(ack.getMessageCount()); 284 destination.getDestinationStatistics().getInflight().subtract(ack.getMessageCount()); 285 if (info.isNetworkSubscription()) { 286 destination.getDestinationStatistics().getForwards().add(ack.getMessageCount()); 287 } 288 } 289 dequeueCounter.addAndGet(ack.getMessageCount()); 290 } 291 while (true) { 292 int currentExtension = prefetchExtension.get(); 293 int newExtension = Math.max(0, currentExtension - ack.getMessageCount()); 294 if (prefetchExtension.compareAndSet(currentExtension, newExtension)) { 295 break; 296 } 297 } 298 dispatchMatched(); 299 return; 300 } else if (ack.isDeliveredAck()) { 301 // Message was delivered but not acknowledged: update pre-fetch counters. 302 prefetchExtension.addAndGet(ack.getMessageCount()); 303 dispatchMatched(); 304 return; 305 } else if (ack.isExpiredAck()) { 306 if (singleDestination && destination != null) { 307 destination.getDestinationStatistics().getInflight().subtract(ack.getMessageCount()); 308 destination.getDestinationStatistics().getExpired().add(ack.getMessageCount()); 309 destination.getDestinationStatistics().getDequeues().add(ack.getMessageCount()); 310 } 311 dequeueCounter.addAndGet(ack.getMessageCount()); 312 while (true) { 313 int currentExtension = prefetchExtension.get(); 314 int newExtension = Math.max(0, currentExtension - ack.getMessageCount()); 315 if (prefetchExtension.compareAndSet(currentExtension, newExtension)) { 316 break; 317 } 318 } 319 dispatchMatched(); 320 return; 321 } else if (ack.isRedeliveredAck()) { 322 // nothing to do atm 323 return; 324 } 325 throw new JMSException("Invalid acknowledgment: " + ack); 326 } 327 328 @Override 329 public Response pullMessage(ConnectionContext context, MessagePull pull) throws Exception { 330 331 // The slave should not deliver pull messages. 332 if (getPrefetchSize() == 0 ) { 333 334 final long currentDispatchedCount = dispatchedCounter.get(); 335 prefetchExtension.incrementAndGet(); 336 dispatchMatched(); 337 338 // If there was nothing dispatched.. we may need to setup a timeout. 339 if (currentDispatchedCount == dispatchedCounter.get()) { 340 341 // immediate timeout used by receiveNoWait() 342 if (pull.getTimeout() == -1) { 343 prefetchExtension.decrementAndGet(); 344 // Send a NULL message to signal nothing pending. 345 dispatch(null); 346 } 347 348 if (pull.getTimeout() > 0) { 349 scheduler.executeAfterDelay(new Runnable() { 350 351 @Override 352 public void run() { 353 pullTimeout(currentDispatchedCount); 354 } 355 }, pull.getTimeout()); 356 } 357 } 358 } 359 return null; 360 } 361 362 /** 363 * Occurs when a pull times out. If nothing has been dispatched since the 364 * timeout was setup, then send the NULL message. 365 */ 366 private final void pullTimeout(long currentDispatchedCount) { 367 synchronized (matchedListMutex) { 368 if (currentDispatchedCount == dispatchedCounter.get()) { 369 try { 370 dispatch(null); 371 } catch (Exception e) { 372 context.getConnection().serviceException(e); 373 } finally { 374 prefetchExtension.decrementAndGet(); 375 } 376 } 377 } 378 } 379 380 @Override 381 public int getPendingQueueSize() { 382 return matched(); 383 } 384 385 @Override 386 public int getDispatchedQueueSize() { 387 return (int)(dispatchedCounter.get() - prefetchExtension.get() - dequeueCounter.get()); 388 } 389 390 public int getMaximumPendingMessages() { 391 return maximumPendingMessages; 392 } 393 394 @Override 395 public long getDispatchedCounter() { 396 return dispatchedCounter.get(); 397 } 398 399 @Override 400 public long getEnqueueCounter() { 401 return enqueueCounter.get(); 402 } 403 404 @Override 405 public long getDequeueCounter() { 406 return dequeueCounter.get(); 407 } 408 409 /** 410 * @return the number of messages discarded due to being a slow consumer 411 */ 412 public int discarded() { 413 synchronized (matchedListMutex) { 414 return discarded; 415 } 416 } 417 418 /** 419 * @return the number of matched messages (messages targeted for the 420 * subscription but not yet able to be dispatched due to the 421 * prefetch buffer being full). 422 */ 423 public int matched() { 424 synchronized (matchedListMutex) { 425 return matched.size(); 426 } 427 } 428 429 /** 430 * Sets the maximum number of pending messages that can be matched against 431 * this consumer before old messages are discarded. 432 */ 433 public void setMaximumPendingMessages(int maximumPendingMessages) { 434 this.maximumPendingMessages = maximumPendingMessages; 435 } 436 437 public MessageEvictionStrategy getMessageEvictionStrategy() { 438 return messageEvictionStrategy; 439 } 440 441 /** 442 * Sets the eviction strategy used to decide which message to evict when the 443 * slow consumer needs to discard messages 444 */ 445 public void setMessageEvictionStrategy(MessageEvictionStrategy messageEvictionStrategy) { 446 this.messageEvictionStrategy = messageEvictionStrategy; 447 } 448 449 public int getMaxProducersToAudit() { 450 return maxProducersToAudit; 451 } 452 453 public synchronized void setMaxProducersToAudit(int maxProducersToAudit) { 454 this.maxProducersToAudit = maxProducersToAudit; 455 if (audit != null) { 456 audit.setMaximumNumberOfProducersToTrack(maxProducersToAudit); 457 } 458 } 459 460 public int getMaxAuditDepth() { 461 return maxAuditDepth; 462 } 463 464 public synchronized void setMaxAuditDepth(int maxAuditDepth) { 465 this.maxAuditDepth = maxAuditDepth; 466 if (audit != null) { 467 audit.setAuditDepth(maxAuditDepth); 468 } 469 } 470 471 public boolean isEnableAudit() { 472 return enableAudit; 473 } 474 475 public synchronized void setEnableAudit(boolean enableAudit) { 476 this.enableAudit = enableAudit; 477 if (enableAudit && audit == null) { 478 audit = new ActiveMQMessageAudit(maxAuditDepth,maxProducersToAudit); 479 } 480 } 481 482 // Implementation methods 483 // ------------------------------------------------------------------------- 484 @Override 485 public boolean isFull() { 486 return getDispatchedQueueSize() >= info.getPrefetchSize(); 487 } 488 489 @Override 490 public int getInFlightSize() { 491 return getDispatchedQueueSize(); 492 } 493 494 /** 495 * @return true when 60% or more room is left for dispatching messages 496 */ 497 @Override 498 public boolean isLowWaterMark() { 499 return getDispatchedQueueSize() <= (info.getPrefetchSize() * .4); 500 } 501 502 /** 503 * @return true when 10% or less room is left for dispatching messages 504 */ 505 @Override 506 public boolean isHighWaterMark() { 507 return getDispatchedQueueSize() >= (info.getPrefetchSize() * .9); 508 } 509 510 /** 511 * @param memoryUsageHighWaterMark the memoryUsageHighWaterMark to set 512 */ 513 public void setMemoryUsageHighWaterMark(int memoryUsageHighWaterMark) { 514 this.memoryUsageHighWaterMark = memoryUsageHighWaterMark; 515 } 516 517 /** 518 * @return the memoryUsageHighWaterMark 519 */ 520 public int getMemoryUsageHighWaterMark() { 521 return this.memoryUsageHighWaterMark; 522 } 523 524 /** 525 * @return the usageManager 526 */ 527 public SystemUsage getUsageManager() { 528 return this.usageManager; 529 } 530 531 /** 532 * @return the matched 533 */ 534 public PendingMessageCursor getMatched() { 535 return this.matched; 536 } 537 538 /** 539 * @param matched the matched to set 540 */ 541 public void setMatched(PendingMessageCursor matched) { 542 this.matched = matched; 543 } 544 545 /** 546 * inform the MessageConsumer on the client to change it's prefetch 547 * 548 * @param newPrefetch 549 */ 550 @Override 551 public void updateConsumerPrefetch(int newPrefetch) { 552 if (context != null && context.getConnection() != null && context.getConnection().isManageable()) { 553 ConsumerControl cc = new ConsumerControl(); 554 cc.setConsumerId(info.getConsumerId()); 555 cc.setPrefetch(newPrefetch); 556 context.getConnection().dispatchAsync(cc); 557 } 558 } 559 560 private void dispatchMatched() throws IOException { 561 synchronized (matchedListMutex) { 562 if (!matched.isEmpty() && !isFull()) { 563 try { 564 matched.reset(); 565 566 while (matched.hasNext() && !isFull()) { 567 MessageReference message = matched.next(); 568 message.decrementReferenceCount(); 569 matched.remove(); 570 // Message may have been sitting in the matched list a while 571 // waiting for the consumer to ak the message. 572 if (message.isExpired()) { 573 discard(message); 574 continue; // just drop it. 575 } 576 dispatch(message); 577 } 578 } finally { 579 matched.release(); 580 } 581 } 582 } 583 } 584 585 private void dispatch(final MessageReference node) throws IOException { 586 Message message = node.getMessage(); 587 if (node != null) { 588 node.incrementReferenceCount(); 589 } 590 // Make sure we can dispatch a message. 591 MessageDispatch md = new MessageDispatch(); 592 md.setMessage(message); 593 md.setConsumerId(info.getConsumerId()); 594 if (node != null) { 595 md.setDestination(((Destination)node.getRegionDestination()).getActiveMQDestination()); 596 dispatchedCounter.incrementAndGet(); 597 // Keep track if this subscription is receiving messages from a single destination. 598 if (singleDestination) { 599 if (destination == null) { 600 destination = (Destination)node.getRegionDestination(); 601 } else { 602 if (destination != node.getRegionDestination()) { 603 singleDestination = false; 604 } 605 } 606 } 607 } 608 if (info.isDispatchAsync()) { 609 if (node != null) { 610 md.setTransmitCallback(new TransmitCallback() { 611 612 @Override 613 public void onSuccess() { 614 Destination regionDestination = (Destination) node.getRegionDestination(); 615 regionDestination.getDestinationStatistics().getDispatched().increment(); 616 regionDestination.getDestinationStatistics().getInflight().increment(); 617 node.decrementReferenceCount(); 618 } 619 620 @Override 621 public void onFailure() { 622 Destination regionDestination = (Destination) node.getRegionDestination(); 623 regionDestination.getDestinationStatistics().getDispatched().increment(); 624 regionDestination.getDestinationStatistics().getInflight().increment(); 625 node.decrementReferenceCount(); 626 } 627 }); 628 } 629 context.getConnection().dispatchAsync(md); 630 } else { 631 context.getConnection().dispatchSync(md); 632 if (node != null) { 633 Destination regionDestination = (Destination) node.getRegionDestination(); 634 regionDestination.getDestinationStatistics().getDispatched().increment(); 635 regionDestination.getDestinationStatistics().getInflight().increment(); 636 node.decrementReferenceCount(); 637 } 638 } 639 } 640 641 private void discard(MessageReference message) { 642 message.decrementReferenceCount(); 643 matched.remove(message); 644 discarded++; 645 if(destination != null) { 646 destination.getDestinationStatistics().getDequeues().increment(); 647 } 648 LOG.debug("{}, discarding message {}", this, message); 649 Destination dest = (Destination) message.getRegionDestination(); 650 if (dest != null) { 651 dest.messageDiscarded(getContext(), this, message); 652 } 653 broker.getRoot().sendToDeadLetterQueue(getContext(), message, this, new Throwable("TopicSubDiscard. ID:" + info.getConsumerId())); 654 } 655 656 @Override 657 public String toString() { 658 return "TopicSubscription:" + " consumer=" + info.getConsumerId() + ", destinations=" + destinations.size() + ", dispatched=" + getDispatchedQueueSize() + ", delivered=" 659 + getDequeueCounter() + ", matched=" + matched() + ", discarded=" + discarded(); 660 } 661 662 @Override 663 public void destroy() { 664 this.active=false; 665 synchronized (matchedListMutex) { 666 try { 667 matched.destroy(); 668 } catch (Exception e) { 669 LOG.warn("Failed to destroy cursor", e); 670 } 671 } 672 setSlowConsumer(false); 673 } 674 675 @Override 676 public int getPrefetchSize() { 677 return info.getPrefetchSize(); 678 } 679 680 @Override 681 public void setPrefetchSize(int newSize) { 682 info.setPrefetchSize(newSize); 683 try { 684 dispatchMatched(); 685 } catch(Exception e) { 686 LOG.trace("Caught exception on dispatch after prefetch size change."); 687 } 688 } 689}