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.camel.util;
018
019import java.io.Serializable;
020import java.util.Collection;
021import java.util.Map;
022import java.util.Set;
023import java.util.concurrent.atomic.AtomicLong;
024
025import com.github.benmanes.caffeine.cache.Cache;
026import com.github.benmanes.caffeine.cache.Caffeine;
027import com.github.benmanes.caffeine.cache.RemovalCause;
028import com.github.benmanes.caffeine.cache.RemovalListener;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032/**
033 * A cache that uses a near optional LRU Cache.
034 * <p/>
035 * The Cache is implemented by Caffeine which provides an <a href="https://github.com/ben-manes/caffeine/wiki/Efficiency">efficient cache</a>.
036 * <p/>
037 * If this cache stores {@link org.apache.camel.Service} then this implementation will on eviction
038 * invoke the {@link org.apache.camel.Service#stop()} method, to auto-stop the service.
039 *
040 * @see LRUSoftCache
041 * @see LRUWeakCache
042 */
043public class LRUCache<K, V> implements Map<K, V>, RemovalListener<K, V>, Serializable {
044    private static final Logger LOG = LoggerFactory.getLogger(LRUCache.class);
045
046    protected final AtomicLong hits = new AtomicLong();
047    protected final AtomicLong misses = new AtomicLong();
048    protected final AtomicLong evicted = new AtomicLong();
049
050    private int maxCacheSize = 10000;
051    private boolean stopOnEviction;
052    private final Cache<K, V> cache;
053    private final Map<K, V> map;
054
055    /**
056     * Constructs an empty <tt>LRUCache</tt> instance with the
057     * specified maximumCacheSize, and will stop on eviction.
058     *
059     * @param maximumCacheSize the max capacity.
060     * @throws IllegalArgumentException if the initial capacity is negative
061     */
062    public LRUCache(int maximumCacheSize) {
063        this(16, maximumCacheSize); // 16 is the default initial capacity in ConcurrentLinkedHashMap
064    }
065
066    /**
067     * Constructs an empty <tt>LRUCache</tt> instance with the
068     * specified initial capacity, maximumCacheSize, and will stop on eviction.
069     *
070     * @param initialCapacity  the initial capacity.
071     * @param maximumCacheSize the max capacity.
072     * @throws IllegalArgumentException if the initial capacity is negative
073     */
074    public LRUCache(int initialCapacity, int maximumCacheSize) {
075        this(initialCapacity, maximumCacheSize, true);
076    }
077
078    /**
079     * Constructs an empty <tt>LRUCache</tt> instance with the
080     * specified initial capacity, maximumCacheSize,load factor and ordering mode.
081     *
082     * @param initialCapacity  the initial capacity.
083     * @param maximumCacheSize the max capacity.
084     * @param stopOnEviction   whether to stop service on eviction.
085     * @throws IllegalArgumentException if the initial capacity is negative
086     */
087    public LRUCache(int initialCapacity, int maximumCacheSize, boolean stopOnEviction) {
088        this(initialCapacity, maximumCacheSize, stopOnEviction, false, false, false);
089    }
090
091    /**
092     * Constructs an empty <tt>LRUCache</tt> instance with the
093     * specified initial capacity, maximumCacheSize,load factor and ordering mode.
094     *
095     * @param initialCapacity  the initial capacity.
096     * @param maximumCacheSize the max capacity.
097     * @param stopOnEviction   whether to stop service on eviction.
098     * @param soft             whether to use soft values a soft cache  (default is false)
099     * @param weak             whether to use weak keys/values as a weak cache  (default is false)
100     * @param syncListener     whether to use synchronous call for the eviction listener (default is false)
101     * @throws IllegalArgumentException if the initial capacity is negative
102     */
103    public LRUCache(int initialCapacity, int maximumCacheSize, boolean stopOnEviction,
104                    boolean soft, boolean weak, boolean syncListener) {
105        Caffeine<K, V> caffeine = Caffeine.newBuilder()
106                .initialCapacity(initialCapacity)
107                .maximumSize(maximumCacheSize)
108                .removalListener(this);
109        if (soft) {
110            caffeine.softValues();
111        }
112        if (weak) {
113            caffeine.weakKeys();
114            caffeine.weakValues();
115        }
116        if (syncListener) {
117            caffeine.executor(Runnable::run);
118        }
119
120        this.cache = caffeine.build();
121        this.map = cache.asMap();
122        this.maxCacheSize = maximumCacheSize;
123        this.stopOnEviction = stopOnEviction;
124    }
125
126    @Override
127    public V get(Object o) {
128        V answer = map.get(o);
129        if (answer != null) {
130            hits.incrementAndGet();
131        } else {
132            misses.incrementAndGet();
133        }
134        return answer;
135    }
136
137    @Override
138    public int size() {
139        return map.size();
140    }
141
142    @Override
143    public boolean isEmpty() {
144        return map.isEmpty();
145    }
146
147    @Override
148    public boolean containsKey(Object o) {
149        return map.containsKey(o);
150    }
151
152    @Override
153    public boolean containsValue(Object o) {
154        return map.containsValue(0);
155    }
156
157    @Override
158    public V put(K k, V v) {
159        return map.put(k, v);
160    }
161
162    @Override
163    public V remove(Object o) {
164        return map.remove(o);
165    }
166
167    public void putAll(Map<? extends K, ? extends V> map) {
168        this.cache.putAll(map);
169    }
170
171    @Override
172    public void clear() {
173        map.clear();
174        resetStatistics();
175    }
176
177    @Override
178    public Set<K> keySet() {
179        return map.keySet();
180    }
181
182    @Override
183    public Collection<V> values() {
184        return map.values();
185    }
186
187    @Override
188    public Set<Entry<K, V>> entrySet() {
189        return map.entrySet();
190    }
191
192    @Override
193    public void onRemoval(K key, V value, RemovalCause cause) {
194        if (cause.wasEvicted()) {
195            evicted.incrementAndGet();
196            LOG.trace("onRemoval {} -> {}", key, value);
197            if (stopOnEviction) {
198                try {
199                    // stop service as its evicted from cache
200                    ServiceHelper.stopService(value);
201                } catch (Exception e) {
202                    LOG.warn("Error stopping service: " + value + ". This exception will be ignored.", e);
203                }
204            }
205        }
206    }
207
208    /**
209     * Gets the number of cache hits
210     */
211    public long getHits() {
212        return hits.get();
213    }
214
215    /**
216     * Gets the number of cache misses.
217     */
218    public long getMisses() {
219        return misses.get();
220    }
221
222    /**
223     * Gets the number of evicted entries.
224     */
225    public long getEvicted() {
226        return evicted.get();
227    }
228
229    /**
230     * Returns the maxCacheSize.
231     */
232    public int getMaxCacheSize() {
233        return maxCacheSize;
234    }
235
236    /**
237     * Rest the cache statistics such as hits and misses.
238     */
239    public void resetStatistics() {
240        hits.set(0);
241        misses.set(0);
242        evicted.set(0);
243    }
244
245    public void cleanUp() {
246        cache.cleanUp();
247    }
248
249    @Override
250    public String toString() {
251        return "LRUCache@" + ObjectHelper.getIdentityHashCode(this);
252    }
253}