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 org.apache.activemq.broker.Broker;
020import org.apache.activemq.broker.BrokerFilter;
021import org.apache.activemq.broker.ConnectionContext;
022import org.apache.activemq.broker.region.Subscription;
023import org.apache.activemq.command.ConsumerInfo;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027import java.io.File;
028import java.io.FileInputStream;
029import java.io.FileOutputStream;
030import java.io.IOException;
031import java.io.ObjectInputStream;
032import java.io.ObjectOutputStream;
033import java.util.Collections;
034import java.util.HashSet;
035import java.util.Set;
036import java.util.concurrent.ConcurrentHashMap;
037
038/**
039 * A plugin which allows the caching of the selector from a subscription queue.
040 * <p/>
041 * This stops the build-up of unwanted messages, especially when consumers may
042 * disconnect from time to time when using virtual destinations.
043 * <p/>
044 * This is influenced by code snippets developed by Maciej Rakowicz
045 *
046 * @author Roelof Naude roelof(dot)naude(at)gmail.com
047 * @see https://issues.apache.org/activemq/browse/AMQ-3004
048 * @see http://mail-archives.apache.org/mod_mbox/activemq-users/201011.mbox/%3C8A013711-2613-450A-A487-379E784AF1D6@homeaway.co.uk%3E
049 */
050public class SubQueueSelectorCacheBroker extends BrokerFilter implements Runnable {
051    private static final Logger LOG = LoggerFactory.getLogger(SubQueueSelectorCacheBroker.class);
052
053    /**
054     * The subscription's selector cache. We cache compiled expressions keyed
055     * by the target destination.
056     */
057    private ConcurrentHashMap<String, Set<String>> subSelectorCache = new ConcurrentHashMap<String, Set<String>>();
058
059    private final File persistFile;
060
061    private boolean running = true;
062    private Thread persistThread;
063    private static final long MAX_PERSIST_INTERVAL = 600000;
064    private static final String SELECTOR_CACHE_PERSIST_THREAD_NAME = "SelectorCachePersistThread";
065
066    /**
067     * Constructor
068     */
069    public SubQueueSelectorCacheBroker(Broker next, final File persistFile) {
070        super(next);
071        this.persistFile = persistFile;
072        LOG.info("Using persisted selector cache from[{}]", persistFile);
073
074        readCache();
075
076        persistThread = new Thread(this, SELECTOR_CACHE_PERSIST_THREAD_NAME);
077        persistThread.start();
078    }
079
080    @Override
081    public void stop() throws Exception {
082        running = false;
083        if (persistThread != null) {
084            persistThread.interrupt();
085            persistThread.join();
086        } //if
087    }
088
089    @Override
090    public Subscription addConsumer(ConnectionContext context, ConsumerInfo info) throws Exception {
091        String destinationName = info.getDestination().getQualifiedName();
092        LOG.debug("Caching consumer selector [{}] on a {}", info.getSelector(), destinationName);
093        String selector = info.getSelector();
094
095        // As ConcurrentHashMap doesn't support null values, use always true expression
096        if (selector == null) {
097            selector = "TRUE";
098        }
099
100        Set<String> selectors = subSelectorCache.get(destinationName);
101        if (selectors == null) {
102            selectors = Collections.synchronizedSet(new HashSet<String>());
103        }
104        selectors.add(selector);
105        subSelectorCache.put(destinationName, selectors);
106
107        return super.addConsumer(context, info);
108    }
109
110    private void readCache() {
111        if (persistFile != null && persistFile.exists()) {
112            try {
113                FileInputStream fis = new FileInputStream(persistFile);
114                try {
115                    ObjectInputStream in = new ObjectInputStream(fis);
116                    try {
117                        subSelectorCache = (ConcurrentHashMap<String, Set<String>>) in.readObject();
118                    } catch (ClassNotFoundException ex) {
119                        LOG.error("Invalid selector cache data found. Please remove file.", ex);
120                    } finally {
121                        in.close();
122                    } //try
123                } finally {
124                    fis.close();
125                } //try
126            } catch (IOException ex) {
127                LOG.error("Unable to read persisted selector cache...it will be ignored!", ex);
128            } //try
129        } //if
130    }
131
132    /**
133     * Persist the selector cache.
134     */
135    private void persistCache() {
136        LOG.debug("Persisting selector cache....");
137        try {
138            FileOutputStream fos = new FileOutputStream(persistFile);
139            try {
140                ObjectOutputStream out = new ObjectOutputStream(fos);
141                try {
142                    out.writeObject(subSelectorCache);
143                } finally {
144                    out.flush();
145                    out.close();
146                } //try
147            } catch (IOException ex) {
148                LOG.error("Unable to persist selector cache", ex);
149            } finally {
150                fos.close();
151            } //try
152        } catch (IOException ex) {
153            LOG.error("Unable to access file[{}]", persistFile, ex);
154        } //try
155    }
156
157    /**
158     * @return The JMS selector for the specified {@code destination}
159     */
160    public Set<String> getSelector(final String destination) {
161        return subSelectorCache.get(destination);
162    }
163
164    /**
165     * Persist the selector cache every {@code MAX_PERSIST_INTERVAL}ms.
166     *
167     * @see java.lang.Runnable#run()
168     */
169    public void run() {
170        while (running) {
171            try {
172                Thread.sleep(MAX_PERSIST_INTERVAL);
173            } catch (InterruptedException ex) {
174            } //try
175
176            persistCache();
177        }
178    }
179}
180