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 LOG.debug("Reading selector cache...."); 198 subSelectorCache = (ConcurrentHashMap<String, Set<String>>) in.readObject(); 199 200 if (LOG.isDebugEnabled()) { 201 final StringBuilder sb = new StringBuilder(); 202 sb.append("Selector cache data loaded from: ").append(persistFile.getAbsolutePath()).append("\n"); 203 sb.append("The following entries were loaded from the cache file: \n"); 204 205 subSelectorCache.forEach((k,v) -> { 206 sb.append("\t").append(k).append(": ").append(v).append("\n"); 207 }); 208 209 LOG.debug(sb.toString()); 210 } 211 } catch (ClassNotFoundException ex) { 212 LOG.error("Invalid selector cache data found. Please remove file.", ex); 213 } finally { 214 in.close(); 215 } 216 } 217 } catch (IOException ex) { 218 LOG.error("Unable to read persisted selector cache...it will be ignored!", ex); 219 } 220 } 221 } 222 223 /** 224 * Persist the selector cache. 225 */ 226 private void persistCache() { 227 LOG.debug("Persisting selector cache...."); 228 try { 229 FileOutputStream fos = new FileOutputStream(persistFile); 230 try { 231 ObjectOutputStream out = new ObjectOutputStream(fos); 232 try { 233 out.writeObject(subSelectorCache); 234 } finally { 235 out.flush(); 236 out.close(); 237 } 238 } catch (IOException ex) { 239 LOG.error("Unable to persist selector cache", ex); 240 } finally { 241 fos.close(); 242 } 243 } catch (IOException ex) { 244 LOG.error("Unable to access file[{}]", persistFile, ex); 245 } 246 } 247 248 /** 249 * @return The JMS selector for the specified {@code destination} 250 */ 251 public Set<String> getSelector(final String destination) { 252 return subSelectorCache.get(destination); 253 } 254 255 /** 256 * Persist the selector cache every {@code MAX_PERSIST_INTERVAL}ms. 257 * 258 * @see java.lang.Runnable#run() 259 */ 260 @Override 261 public void run() { 262 while (running) { 263 try { 264 Thread.sleep(persistInterval); 265 } catch (InterruptedException ex) { 266 } 267 268 persistCache(); 269 } 270 } 271 272 public boolean isSingleSelectorPerDestination() { 273 return singleSelectorPerDestination; 274 } 275 276 public void setSingleSelectorPerDestination(boolean singleSelectorPerDestination) { 277 this.singleSelectorPerDestination = singleSelectorPerDestination; 278 } 279 280 @SuppressWarnings("unchecked") 281 public Set<String> getSelectorsForDestination(String destinationName) { 282 if (subSelectorCache.containsKey(destinationName)) { 283 return new HashSet<String>(subSelectorCache.get(destinationName)); 284 } 285 286 return Collections.EMPTY_SET; 287 } 288 289 public long getPersistInterval() { 290 return persistInterval; 291 } 292 293 public void setPersistInterval(long persistInterval) { 294 this.persistInterval = persistInterval; 295 } 296 297 public boolean deleteSelectorForDestination(String destinationName, String selector) { 298 if (subSelectorCache.containsKey(destinationName)) { 299 Set<String> cachedSelectors = subSelectorCache.get(destinationName); 300 return cachedSelectors.remove(selector); 301 } 302 303 return false; 304 } 305 306 public boolean deleteAllSelectorsForDestination(String destinationName) { 307 if (subSelectorCache.containsKey(destinationName)) { 308 Set<String> cachedSelectors = subSelectorCache.get(destinationName); 309 cachedSelectors.clear(); 310 } 311 return true; 312 } 313 314 public boolean isIgnoreWildcardSelectors() { 315 return ignoreWildcardSelectors; 316 } 317 318 public void setIgnoreWildcardSelectors(boolean ignoreWildcardSelectors) { 319 this.ignoreWildcardSelectors = ignoreWildcardSelectors; 320 } 321 322 // find wildcards inside like operator arguments 323 static class WildcardFinder { 324 325 private static final Pattern LIKE_PATTERN=Pattern.compile( 326 "\\bLIKE\\s+'(?<like>([^']|'')+)'(\\s+ESCAPE\\s+'(?<escape>.)')?", 327 Pattern.CASE_INSENSITIVE); 328 329 private static final String REGEX_SPECIAL = ".+?*(){}[]\\-"; 330 331 private static String getLike(final Matcher matcher) { 332 return matcher.group("like"); 333 } 334 335 private static boolean hasLikeOperator(final Matcher matcher) { 336 return matcher.find(); 337 } 338 339 private static String getEscape(final Matcher matcher) { 340 String escapeChar = matcher.group("escape"); 341 if (escapeChar == null) { 342 return null; 343 } else if (REGEX_SPECIAL.contains(escapeChar)) { 344 escapeChar = "\\"+escapeChar; 345 } 346 return escapeChar; 347 } 348 349 private static boolean hasWildcardInCurrentMatch(final Matcher matcher) { 350 String wildcards = "[_%]"; 351 if (getEscape(matcher) != null) { 352 wildcards = "(^|[^" + getEscape(matcher) + "])" + wildcards; 353 } 354 return Pattern.compile(wildcards).matcher(getLike(matcher)).find(); 355 } 356 357 public static boolean hasWildcards(String selector) { 358 Matcher matcher = LIKE_PATTERN.matcher(selector); 359 360 while(hasLikeOperator(matcher)) { 361 if (hasWildcardInCurrentMatch(matcher)) { 362 return true; 363 } 364 } 365 return false; 366 } 367 } 368}