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.plugin; 018 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.FileOutputStream; 022import java.io.IOException; 023import java.io.ObjectInputStream; 024import java.io.ObjectOutputStream; 025import java.util.Collections; 026import java.util.HashSet; 027import java.util.Set; 028import java.util.concurrent.ConcurrentHashMap; 029import java.util.concurrent.ConcurrentMap; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033import javax.management.JMException; 034import javax.management.ObjectName; 035 036import org.apache.activemq.advisory.AdvisorySupport; 037import org.apache.activemq.broker.Broker; 038import org.apache.activemq.broker.BrokerFilter; 039import org.apache.activemq.broker.BrokerService; 040import org.apache.activemq.broker.ConnectionContext; 041import org.apache.activemq.broker.jmx.AnnotatedMBean; 042import org.apache.activemq.broker.jmx.BrokerMBeanSupport; 043import org.apache.activemq.broker.jmx.VirtualDestinationSelectorCacheView; 044import org.apache.activemq.broker.region.Subscription; 045import org.apache.activemq.command.ConsumerInfo; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048 049/** 050 * A plugin which allows the caching of the selector from a subscription queue. 051 * <p/> 052 * This stops the build-up of unwanted messages, especially when consumers may 053 * disconnect from time to time when using virtual destinations. 054 * <p/> 055 * This is influenced by code snippets developed by Maciej Rakowicz 056 * 057 * Refer to: 058 * https://issues.apache.org/activemq/browse/AMQ-3004 059 * http://mail-archives.apache.org/mod_mbox/activemq-users/201011.mbox/%3C8A013711-2613-450A-A487-379E784AF1D6@homeaway.co.uk%3E 060 */ 061public class SubQueueSelectorCacheBroker extends BrokerFilter implements Runnable { 062 private static final Logger LOG = LoggerFactory.getLogger(SubQueueSelectorCacheBroker.class); 063 public static final String MATCH_EVERYTHING = "TRUE"; 064 065 /** 066 * The subscription's selector cache. We cache compiled expressions keyed 067 * by the target destination. 068 */ 069 private ConcurrentMap<String, Set<String>> subSelectorCache = new ConcurrentHashMap<String, Set<String>>(); 070 071 private final File persistFile; 072 private boolean singleSelectorPerDestination = false; 073 private boolean ignoreWildcardSelectors = false; 074 private ObjectName objectName; 075 076 private boolean running = true; 077 private final Thread persistThread; 078 private long persistInterval = MAX_PERSIST_INTERVAL; 079 public static final long MAX_PERSIST_INTERVAL = 600000; 080 private static final String SELECTOR_CACHE_PERSIST_THREAD_NAME = "SelectorCachePersistThread"; 081 082 /** 083 * Constructor 084 */ 085 public SubQueueSelectorCacheBroker(Broker next, final File persistFile) { 086 super(next); 087 this.persistFile = persistFile; 088 LOG.info("Using persisted selector cache from[{}]", persistFile); 089 090 readCache(); 091 092 persistThread = new Thread(this, SELECTOR_CACHE_PERSIST_THREAD_NAME); 093 persistThread.start(); 094 enableJmx(); 095 } 096 097 private void enableJmx() { 098 BrokerService broker = getBrokerService(); 099 if (broker.isUseJmx()) { 100 VirtualDestinationSelectorCacheView view = new VirtualDestinationSelectorCacheView(this); 101 try { 102 objectName = BrokerMBeanSupport.createVirtualDestinationSelectorCacheName(broker.getBrokerObjectName(), "plugin", "virtualDestinationCache"); 103 LOG.trace("virtualDestinationCacheSelector mbean name; " + objectName.toString()); 104 AnnotatedMBean.registerMBean(broker.getManagementContext(), view, objectName); 105 } catch (Exception e) { 106 LOG.warn("JMX is enabled, but when installing the VirtualDestinationSelectorCache, couldn't install the JMX mbeans. Continuing without installing the mbeans."); 107 } 108 } 109 } 110 111 @Override 112 public void stop() throws Exception { 113 running = false; 114 if (persistThread != null) { 115 persistThread.interrupt(); 116 persistThread.join(); 117 } 118 unregisterMBeans(); 119 } 120 121 private void unregisterMBeans() { 122 BrokerService broker = getBrokerService(); 123 if (broker.isUseJmx() && this.objectName != null) { 124 try { 125 broker.getManagementContext().unregisterMBean(objectName); 126 } catch (JMException e) { 127 LOG.warn("Trying uninstall VirtualDestinationSelectorCache; couldn't uninstall mbeans, continuting..."); 128 } 129 } 130 } 131 132 @Override 133 public Subscription addConsumer(ConnectionContext context, ConsumerInfo info) throws Exception { 134 // don't track selectors for advisory topics, temp destinations or console 135 // related consumers 136 if (!AdvisorySupport.isAdvisoryTopic(info.getDestination()) && !info.getDestination().isTemporary() 137 && !info.isBrowser()) { 138 String destinationName = info.getDestination().getQualifiedName(); 139 LOG.debug("Caching consumer selector [{}] on '{}'", info.getSelector(), destinationName); 140 141 String selector = info.getSelector() == null ? MATCH_EVERYTHING : info.getSelector(); 142 143 if (!(ignoreWildcardSelectors && hasWildcards(selector))) { 144 145 Set<String> selectors = subSelectorCache.get(destinationName); 146 if (selectors == null) { 147 selectors = Collections.synchronizedSet(new HashSet<String>()); 148 } else if (singleSelectorPerDestination && !MATCH_EVERYTHING.equals(selector)) { 149 // in this case, we allow only ONE selector. But we don't count the catch-all "null/TRUE" selector 150 // here, we always allow that one. But only one true selector. 151 boolean containsMatchEverything = selectors.contains(MATCH_EVERYTHING); 152 selectors.clear(); 153 154 // put back the MATCH_EVERYTHING selector 155 if (containsMatchEverything) { 156 selectors.add(MATCH_EVERYTHING); 157 } 158 } 159 160 LOG.debug("adding new selector: into cache " + selector); 161 selectors.add(selector); 162 LOG.debug("current selectors in cache: " + selectors); 163 subSelectorCache.put(destinationName, selectors); 164 } 165 } 166 167 return super.addConsumer(context, info); 168 } 169 170 static boolean hasWildcards(String selector) { 171 return WildcardFinder.hasWildcards(selector); 172 } 173 174 @Override 175 public void removeConsumer(ConnectionContext context, ConsumerInfo info) throws Exception { 176 if (!AdvisorySupport.isAdvisoryTopic(info.getDestination()) && !info.getDestination().isTemporary()) { 177 if (singleSelectorPerDestination) { 178 String destinationName = info.getDestination().getQualifiedName(); 179 Set<String> selectors = subSelectorCache.get(destinationName); 180 if (info.getSelector() == null && selectors.size() > 1) { 181 boolean removed = selectors.remove(MATCH_EVERYTHING); 182 LOG.debug("A non-selector consumer has dropped. Removing the catchall matching pattern 'TRUE'. Successful? " + removed); 183 } 184 } 185 186 } 187 super.removeConsumer(context, info); 188 } 189 190 @SuppressWarnings("unchecked") 191 private void readCache() { 192 if (persistFile != null && persistFile.exists()) { 193 try { 194 try (FileInputStream fis = new FileInputStream(persistFile);) { 195 ObjectInputStream in = new ObjectInputStream(fis); 196 try { 197 subSelectorCache = (ConcurrentHashMap<String, Set<String>>) in.readObject(); 198 } catch (ClassNotFoundException ex) { 199 LOG.error("Invalid selector cache data found. Please remove file.", ex); 200 } finally { 201 in.close(); 202 } 203 } 204 } catch (IOException ex) { 205 LOG.error("Unable to read persisted selector cache...it will be ignored!", ex); 206 } 207 } 208 } 209 210 /** 211 * Persist the selector cache. 212 */ 213 private void persistCache() { 214 LOG.debug("Persisting selector cache...."); 215 try { 216 FileOutputStream fos = new FileOutputStream(persistFile); 217 try { 218 ObjectOutputStream out = new ObjectOutputStream(fos); 219 try { 220 out.writeObject(subSelectorCache); 221 } finally { 222 out.flush(); 223 out.close(); 224 } 225 } catch (IOException ex) { 226 LOG.error("Unable to persist selector cache", ex); 227 } finally { 228 fos.close(); 229 } 230 } catch (IOException ex) { 231 LOG.error("Unable to access file[{}]", persistFile, ex); 232 } 233 } 234 235 /** 236 * @return The JMS selector for the specified {@code destination} 237 */ 238 public Set<String> getSelector(final String destination) { 239 return subSelectorCache.get(destination); 240 } 241 242 /** 243 * Persist the selector cache every {@code MAX_PERSIST_INTERVAL}ms. 244 * 245 * @see java.lang.Runnable#run() 246 */ 247 @Override 248 public void run() { 249 while (running) { 250 try { 251 Thread.sleep(persistInterval); 252 } catch (InterruptedException ex) { 253 } 254 255 persistCache(); 256 } 257 } 258 259 public boolean isSingleSelectorPerDestination() { 260 return singleSelectorPerDestination; 261 } 262 263 public void setSingleSelectorPerDestination(boolean singleSelectorPerDestination) { 264 this.singleSelectorPerDestination = singleSelectorPerDestination; 265 } 266 267 @SuppressWarnings("unchecked") 268 public Set<String> getSelectorsForDestination(String destinationName) { 269 if (subSelectorCache.containsKey(destinationName)) { 270 return new HashSet<String>(subSelectorCache.get(destinationName)); 271 } 272 273 return Collections.EMPTY_SET; 274 } 275 276 public long getPersistInterval() { 277 return persistInterval; 278 } 279 280 public void setPersistInterval(long persistInterval) { 281 this.persistInterval = persistInterval; 282 } 283 284 public boolean deleteSelectorForDestination(String destinationName, String selector) { 285 if (subSelectorCache.containsKey(destinationName)) { 286 Set<String> cachedSelectors = subSelectorCache.get(destinationName); 287 return cachedSelectors.remove(selector); 288 } 289 290 return false; 291 } 292 293 public boolean deleteAllSelectorsForDestination(String destinationName) { 294 if (subSelectorCache.containsKey(destinationName)) { 295 Set<String> cachedSelectors = subSelectorCache.get(destinationName); 296 cachedSelectors.clear(); 297 } 298 return true; 299 } 300 301 public boolean isIgnoreWildcardSelectors() { 302 return ignoreWildcardSelectors; 303 } 304 305 public void setIgnoreWildcardSelectors(boolean ignoreWildcardSelectors) { 306 this.ignoreWildcardSelectors = ignoreWildcardSelectors; 307 } 308 309 // find wildcards inside like operator arguments 310 static class WildcardFinder { 311 312 private static final Pattern LIKE_PATTERN=Pattern.compile( 313 "\\bLIKE\\s+'(?<like>([^']|'')+)'(\\s+ESCAPE\\s+'(?<escape>.)')?", 314 Pattern.CASE_INSENSITIVE); 315 316 private static final String REGEX_SPECIAL = ".+?*(){}[]\\-"; 317 318 private static String getLike(final Matcher matcher) { 319 return matcher.group("like"); 320 } 321 322 private static boolean hasLikeOperator(final Matcher matcher) { 323 return matcher.find(); 324 } 325 326 private static String getEscape(final Matcher matcher) { 327 String escapeChar = matcher.group("escape"); 328 if (escapeChar == null) { 329 return null; 330 } else if (REGEX_SPECIAL.contains(escapeChar)) { 331 escapeChar = "\\"+escapeChar; 332 } 333 return escapeChar; 334 } 335 336 private static boolean hasWildcardInCurrentMatch(final Matcher matcher) { 337 String wildcards = "[_%]"; 338 if (getEscape(matcher) != null) { 339 wildcards = "(^|[^" + getEscape(matcher) + "])" + wildcards; 340 } 341 return Pattern.compile(wildcards).matcher(getLike(matcher)).find(); 342 } 343 344 public static boolean hasWildcards(String selector) { 345 Matcher matcher = LIKE_PATTERN.matcher(selector); 346 347 while(hasLikeOperator(matcher)) { 348 if (hasWildcardInCurrentMatch(matcher)) { 349 return true; 350 } 351 } 352 return false; 353 } 354 } 355}