/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jane.core.map; import java.util.Iterator; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import jane.core.Log; import jane.core.map.LRUCleaner.Cleanable; /** * A LRU cache implementation based upon LongConcurrentHashMap and other techniques to reduce * contention and synchronization overhead to utilize multiple CPU cores more effectively. * <p/> * Note that the implementation does not follow a true LRU (least-recently-used) eviction strategy. * Instead it strives to remove least recently used items but when the initial cleanup does not remove enough items * to reach the 'acceptSize' limit, it can remove more items forcefully regardless of access order. * * MapDB note: reworked to implement LongMap. Original comes from: * https://svn.apache.org/repos/asf/lucene/dev/trunk/solr/core/src/java/org/apache/solr/util/ConcurrentLRUCache.java */ public final class LongConcurrentLRUMap<V> extends LongMap<V> implements Cleanable { private static final int UPPERSIZE_MIN = 1024; private final LongConcurrentHashMap<CacheEntry<V>> map; private final AtomicLong versionCounter = new AtomicLong(); private final AtomicInteger size = new AtomicInteger(); private final AtomicInteger sweepStatus = new AtomicInteger(); private final int upperSize; private final int lowerSize; private final int acceptSize; private final String name; private long minVersion; public LongConcurrentLRUMap(int upperSize, int lowerSize, int acceptSize, int initialSize, float loadFactor, int concurrencyLevel, String name) { if(lowerSize <= 0) throw new IllegalArgumentException("lowerSize must be > 0"); if(upperSize <= lowerSize) throw new IllegalArgumentException("upperSize must be > lowerSize"); map = new LongConcurrentHashMap<>(initialSize, loadFactor, concurrencyLevel); this.upperSize = upperSize; this.lowerSize = lowerSize; this.acceptSize = acceptSize; this.name = name; } public LongConcurrentLRUMap(int lowerSize, float loadFactor, int concurrencyLevel, String name) { this(Math.max(lowerSize + (lowerSize + 1) / 2, UPPERSIZE_MIN), lowerSize, lowerSize + lowerSize / 4, Math.max(lowerSize + (lowerSize + 1) / 2, UPPERSIZE_MIN) + 256, loadFactor, concurrencyLevel, name); } private static final class CacheEntry<V> extends CacheEntryBase<V> { private final long key; private CacheEntry(long k, V v, long ver) { key = k; value = v; version = ver; } } @Override public boolean isEmpty() { return map.isEmpty(); } @Override public int size() { return size.get(); } @Override public V get(long key) { CacheEntry<V> e = map.get(key); if(e == null) return null; e.version = versionCounter.incrementAndGet(); return e.value; } @Override public V put(long key, V value) { if(value == null) return null; CacheEntry<V> ceOld = map.put(key, new CacheEntry<>(key, value, versionCounter.incrementAndGet())); if(ceOld != null) return ceOld.value; if(size.incrementAndGet() > upperSize && sweepStatus.get() == 0) LRUCleaner.submit(sweepStatus, this); return null; } @Override public V remove(long key) { CacheEntry<V> ceOld = map.remove(key); if(ceOld == null) return null; size.decrementAndGet(); return ceOld.value; } @Override public boolean remove(long key, V value) { if(!map.remove(key, value)) return false; size.decrementAndGet(); return true; } @Override public void clear() { map.clear(); size.set(0); } private void evictEntry(long key) { CacheEntry<V> o = map.remove(key); if(o == null) return; size.decrementAndGet(); // evictedEntry(o.key, o.value); } /** override this method to get notified about evicted entries*/ // private void evictedEntry(long key, V value) {} /** * Removes items from the cache to bring the size down to 'acceptSize'. * <p/> * It is done in two stages. In the first stage, least recently used items are evicted. * If after the first stage, the cache size is still greater than 'acceptSize', the second stage takes over. * <p/> * The second stage is more intensive and tries to bring down the cache size to the 'lowerSize'. */ @Override public void sweep() { // if we want to keep at least 1000 entries, then timestamps of current through current-1000 // are guaranteed not to be the oldest (but that does not mean there are 1000 entries in that group... // it's acutally anywhere between 1 and 1000). // Also, if we want to remove 500 entries, then oldestEntry through oldestEntry+500 // are guaranteed to be removed (however many there are there). if(!sweepStatus.compareAndSet(1, 2)) return; final long time = (Log.hasDebug ? System.currentTimeMillis() : 0); final int sizeOld = size.get(); try { final long curV = versionCounter.get(); long minV = minVersion; long maxVNew = -1; long minVNew = Long.MAX_VALUE; int numToKeep = lowerSize; int numToRemove = sizeOld - numToKeep; int numKept = 0; int numRemoved = 0; CacheEntry<?>[] eList = new CacheEntry<?>[sizeOld]; int eSize = 0; for(final Iterator<CacheEntry<V>> it = map.valueIterator(); it.hasNext();) { final CacheEntry<V> ce = it.next(); final long v = ce.version; ce.versionCopy = v; // since the numToKeep group is likely to be bigger than numToRemove, check it first if(v > curV - numToKeep) { // this entry is guaranteed not to be in the bottom group, so do nothing numKept++; if(minVNew > v) minVNew = v; } else if(v < minV + numToRemove) // entry in bottom group? { // this entry is guaranteed to be in the bottom group, so immediately remove it from the map evictEntry(ce.key); numRemoved++; } else if(eSize < sizeOld - 1) { // This entry *could* be in the bottom group. // Collect these entries to avoid another full pass... // this is wasted effort if enough entries are normally removed in this first pass. // An alternate impl could make a full second pass. eList[eSize++] = ce; if(maxVNew < v) maxVNew = v; if(minVNew > v) minVNew = v; } } // int numPasses = 1; // maximum number of linear passes over the data // if we didn't remove enough entries, then make more passes over the values we collected, with updated min and max values if(sizeOld - numRemoved > acceptSize) // while(sizeOld - numRemoved > acceptSize && --numPasses >= 0) { if(minVNew != Long.MAX_VALUE) minV = minVNew; minVNew = Long.MAX_VALUE; final long maxV = maxVNew; maxVNew = -1; numToKeep = lowerSize - numKept; numToRemove = sizeOld - lowerSize - numRemoved; // iterate backward to make it easy to remove items for(int i = eSize - 1; i >= 0; --i) { final CacheEntry<?> ce = eList[i]; final long v = ce.versionCopy; if(v > maxV - numToKeep) { // this entry is guaranteed not to be in the bottom group, so do nothing but remove it from the eList numKept++; eList[i] = eList[--eSize]; // remove the entry by moving the last element to its position if(minVNew > v) minVNew = v; } else if(v < minV + numToRemove) // entry in bottom group? { // this entry is guaranteed to be in the bottom group, so immediately remove it from the map evictEntry(ce.key); numRemoved++; eList[i] = eList[--eSize]; // remove the entry by moving the last element to its position } else { // This entry *could* be in the bottom group, so keep it in the eList, and update the stats if(maxVNew < v) maxVNew = v; if(minVNew > v) minVNew = v; } } } // if we still didn't remove enough entries, then make another pass while inserting into a priority queue if(sizeOld - numRemoved > acceptSize) { if(minVNew != Long.MAX_VALUE) minV = minVNew; minVNew = Long.MAX_VALUE; final long maxV = maxVNew; maxVNew = -1; numToKeep = lowerSize - numKept; numToRemove = sizeOld - lowerSize - numRemoved; final LRUQueue<CacheEntry<?>> queue = new LRUQueue<>(numToRemove, new CacheEntry<?>[LRUQueue.calHeapSize(numToRemove)]); for(int i = eSize - 1; i >= 0; --i) { final CacheEntry<?> ce = eList[i]; final long v = ce.versionCopy; if(v > maxV - numToKeep) { // this entry is guaranteed not to be in the bottom group, so do nothing but remove it from the eList numKept++; if(minVNew > v) minVNew = v; } else if(v < minV + numToRemove) // entry in bottom group? { // this entry is guaranteed to be in the bottom group so immediately remove it evictEntry(ce.key); numRemoved++; } else { // This entry *could* be in the bottom group. add it to the priority queue // everything in the priority queue will be removed, so keep track of // the lowest value that ever comes back out of the queue. // first reduce the size of the priority queue to account for // the number of items we have already removed while executing this loop so far. final int maxSize = sizeOld - lowerSize - numRemoved; queue.maxSize = maxSize; while(queue.size > maxSize && queue.size > 0) { final long otherEntryV = queue.pop().versionCopy; if(minVNew > otherEntryV) minVNew = otherEntryV; } if(maxSize <= 0) break; final CacheEntry<?> o = queue.insertWithOverflow(ce); if(o != null && minVNew > o.versionCopy) minVNew = o.versionCopy; } } // Now delete everything in the priority queue. avoid using pop() since order doesn't matter anymore for(final CacheEntry<?> ce : queue.heap) { if(ce == null) continue; evictEntry(ce.key); numRemoved++; } // System.out.println("numRemoved=" + numRemoved + " numKept=" + numKept + " initialQueueSize="+ numToRemove // + " finalQueueSize=" + queue.size() + " sizeOld-numRemoved=" + (sizeOld-numRemoved)); } minVersion = (minVNew == Long.MAX_VALUE ? minV : minVNew); } finally { if(Log.hasDebug) Log.log.debug("LRUMap.sweep({}: {}=>{}, {}ms)", name, sizeOld, size.get(), System.currentTimeMillis() - time); } } @Override public LongIterator keyIterator() { return map.keyIterator(); } @Override public Iterator<V> valueIterator() { return new ValueIterator<>(map); } @Override public MapIterator<V> entryIterator() { return new EntryIterator<>(map); } private static final class ValueIterator<V> implements Iterator<V> { private final Iterator<CacheEntry<V>> it; private ValueIterator(LongConcurrentHashMap<CacheEntry<V>> map) { it = map.valueIterator(); } @Override public boolean hasNext() { return it.hasNext(); } @Override public V next() { return it.next().value; } @Deprecated @Override public void remove() { throw new UnsupportedOperationException(); } } private static final class EntryIterator<V> implements MapIterator<V> { private final MapIterator<CacheEntry<V>> it; private EntryIterator(LongConcurrentHashMap<CacheEntry<V>> map) { it = map.entryIterator(); } @Override public boolean moveToNext() { return it.moveToNext(); } @Override public long key() { return it.key(); } @Override public V value() { return it.value().value; } @Deprecated @Override public void remove() { throw new UnsupportedOperationException(); } } }