/*
* 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 org.apache.solr.search;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
import com.codahale.metrics.MetricRegistry;
import org.apache.lucene.util.Accountable;
import org.apache.lucene.util.Accountables;
import org.apache.lucene.util.RamUsageEstimator;
import org.apache.solr.common.SolrException;
import org.apache.solr.metrics.MetricsMap;
import org.apache.solr.metrics.SolrMetricManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
*/
public class LRUCache<K,V> extends SolrCacheBase implements SolrCache<K,V>, Accountable {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(LRUCache.class);
/// Copied from Lucene's LRUQueryCache
// memory usage of a simple term query
public static final long DEFAULT_RAM_BYTES_USED = 192;
public static final long HASHTABLE_RAM_BYTES_PER_ENTRY =
2 * RamUsageEstimator.NUM_BYTES_OBJECT_REF // key + value
* 2; // hash tables need to be oversized to avoid collisions, assume 2x capacity
static final long LINKED_HASHTABLE_RAM_BYTES_PER_ENTRY =
HASHTABLE_RAM_BYTES_PER_ENTRY
+ 2 * RamUsageEstimator.NUM_BYTES_OBJECT_REF; // previous & next references
/// End copied code
/* An instance of this class will be shared across multiple instances
* of an LRUCache at the same time. Make sure everything is thread safe.
*/
private static class CumulativeStats {
LongAdder lookups = new LongAdder();
LongAdder hits = new LongAdder();
LongAdder inserts = new LongAdder();
LongAdder evictions = new LongAdder();
LongAdder evictionsRamUsage = new LongAdder();
}
private CumulativeStats stats;
// per instance stats. The synchronization used for the map will also be
// used for updating these statistics (and hence they are not AtomicLongs
private long lookups;
private long hits;
private long inserts;
private long evictions;
private long evictionsRamUsage;
private long warmupTime = 0;
private Map<K,V> map;
private String description="LRU Cache";
private MetricsMap cacheMap;
private Set<String> metricNames = new HashSet<>();
private MetricRegistry registry;
private long maxRamBytes = Long.MAX_VALUE;
// The synchronization used for the map will be used to update this,
// hence not an AtomicLong
private long ramBytesUsed = 0;
@Override
public Object init(Map args, Object persistence, CacheRegenerator regenerator) {
super.init(args, regenerator);
String str = (String)args.get("size");
final int limit = str==null ? 1024 : Integer.parseInt(str);
str = (String)args.get("initialSize");
final int initialSize = Math.min(str==null ? 1024 : Integer.parseInt(str), limit);
str = (String) args.get("maxRamMB");
final long maxRamBytes = this.maxRamBytes = str == null ? Long.MAX_VALUE : (long) (Double.parseDouble(str) * 1024L * 1024L);
description = generateDescription(limit, initialSize);
map = new LinkedHashMap<K,V>(initialSize, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
if (size() > limit || ramBytesUsed > maxRamBytes) {
if (maxRamBytes != Long.MAX_VALUE && ramBytesUsed > maxRamBytes) {
long bytesToDecrement = 0;
Iterator<Map.Entry<K, V>> iterator = entrySet().iterator();
do {
Map.Entry<K, V> entry = iterator.next();
if (entry.getKey() != null) {
if (entry.getKey() instanceof Accountable) {
bytesToDecrement += ((Accountable) entry.getKey()).ramBytesUsed();
} else {
bytesToDecrement += DEFAULT_RAM_BYTES_USED;
}
}
if (entry.getValue() != null) {
bytesToDecrement += ((Accountable) entry.getValue()).ramBytesUsed();
}
bytesToDecrement += LINKED_HASHTABLE_RAM_BYTES_PER_ENTRY;
ramBytesUsed -= bytesToDecrement;
iterator.remove();
evictions++;
evictionsRamUsage++;
stats.evictions.increment();
stats.evictionsRamUsage.increment();
} while (iterator.hasNext() && ramBytesUsed > maxRamBytes);
// must return false according to javadocs of removeEldestEntry if we're modifying
// the map ourselves
return false;
} else {
// increment evictions regardless of state.
// this doesn't need to be synchronized because it will
// only be called in the context of a higher level synchronized block.
evictions++;
stats.evictions.increment();
return true;
}
}
return false;
}
};
if (persistence==null) {
// must be the first time a cache of this type is being created
persistence = new CumulativeStats();
}
stats = (CumulativeStats)persistence;
return persistence;
}
/**
*
* @return Returns the description of this cache.
*/
private String generateDescription(int limit, int initialSize) {
String description = "LRU Cache(maxSize=" + limit + ", initialSize=" + initialSize;
if (isAutowarmingOn()) {
description += ", " + getAutowarmDescription();
}
if (maxRamBytes != Long.MAX_VALUE) {
description += ", maxRamMB=" + (maxRamBytes / 1024L / 1024L);
}
description += ')';
return description;
}
@Override
public int size() {
synchronized(map) {
return map.size();
}
}
@Override
public V put(K key, V value) {
synchronized (map) {
if (getState() == State.LIVE) {
stats.inserts.increment();
}
// increment local inserts regardless of state???
// it does make it more consistent with the current size...
inserts++;
// important to calc and add new ram bytes first so that removeEldestEntry can compare correctly
long keySize = DEFAULT_RAM_BYTES_USED;
if (maxRamBytes != Long.MAX_VALUE) {
if (key != null && key instanceof Accountable) {
keySize = ((Accountable) key).ramBytesUsed();
}
long valueSize = 0;
if (value != null) {
if (value instanceof Accountable) {
Accountable accountable = (Accountable) value;
valueSize = accountable.ramBytesUsed();
} else {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Cache: "
+ getName() + " is configured with maxRamBytes=" + RamUsageEstimator.humanReadableUnits(maxRamBytes)
+ " but its values do not implement org.apache.lucene.util.Accountable");
}
}
ramBytesUsed += keySize + valueSize + LINKED_HASHTABLE_RAM_BYTES_PER_ENTRY;
}
V old = map.put(key, value);
if (maxRamBytes != Long.MAX_VALUE && old != null) {
long bytesToDecrement = ((Accountable) old).ramBytesUsed();
// the key existed in the map but we added its size before the put, so let's back out
bytesToDecrement += LINKED_HASHTABLE_RAM_BYTES_PER_ENTRY;
if (key != null) {
if (key instanceof Accountable) {
Accountable aKey = (Accountable) key;
bytesToDecrement += aKey.ramBytesUsed();
} else {
bytesToDecrement += DEFAULT_RAM_BYTES_USED;
}
}
ramBytesUsed -= bytesToDecrement;
}
return old;
}
}
@Override
public V get(K key) {
synchronized (map) {
V val = map.get(key);
if (getState() == State.LIVE) {
// only increment lookups and hits if we are live.
lookups++;
stats.lookups.increment();
if (val!=null) {
hits++;
stats.hits.increment();
}
}
return val;
}
}
@Override
public void clear() {
synchronized(map) {
map.clear();
ramBytesUsed = 0;
}
}
@Override
public void warm(SolrIndexSearcher searcher, SolrCache<K,V> old) {
if (regenerator==null) return;
long warmingStartTime = System.nanoTime();
LRUCache<K,V> other = (LRUCache<K,V>)old;
// warm entries
if (isAutowarmingOn()) {
Object[] keys,vals = null;
// Don't do the autowarming in the synchronized block, just pull out the keys and values.
synchronized (other.map) {
int sz = autowarm.getWarmCount(other.map.size());
keys = new Object[sz];
vals = new Object[sz];
Iterator<Map.Entry<K, V>> iter = other.map.entrySet().iterator();
// iteration goes from oldest (least recently used) to most recently used,
// so we need to skip over the oldest entries.
int skip = other.map.size() - sz;
for (int i=0; i<skip; i++) iter.next();
for (int i=0; i<sz; i++) {
Map.Entry<K,V> entry = iter.next();
keys[i]=entry.getKey();
vals[i]=entry.getValue();
}
}
// autowarm from the oldest to the newest entries so that the ordering will be
// correct in the new cache.
for (int i=0; i<keys.length; i++) {
try {
boolean continueRegen = regenerator.regenerateItem(searcher, this, old, keys[i], vals[i]);
if (!continueRegen) break;
}
catch (Exception e) {
SolrException.log(log,"Error during auto-warming of key:" + keys[i], e);
}
}
}
warmupTime = TimeUnit.MILLISECONDS.convert(System.nanoTime() - warmingStartTime, TimeUnit.NANOSECONDS);
}
@Override
public void close() {
}
//////////////////////// SolrInfoMBeans methods //////////////////////
@Override
public String getName() {
return LRUCache.class.getName();
}
@Override
public String getDescription() {
return description;
}
@Override
public Set<String> getMetricNames() {
return metricNames;
}
@Override
public void initializeMetrics(SolrMetricManager manager, String registryName, String scope) {
registry = manager.registry(registryName);
cacheMap = new MetricsMap((detailed, res) -> {
synchronized (map) {
res.put("lookups", lookups);
res.put("hits", hits);
res.put("hitratio", calcHitRatio(lookups,hits));
res.put("inserts", inserts);
res.put("evictions", evictions);
res.put("size", map.size());
if (maxRamBytes != Long.MAX_VALUE) {
res.put("maxRamMB", maxRamBytes / 1024L / 1024L);
res.put("ramBytesUsed", ramBytesUsed());
res.put("evictionsRamUsage", evictionsRamUsage);
}
}
res.put("warmupTime", warmupTime);
long clookups = stats.lookups.longValue();
long chits = stats.hits.longValue();
res.put("cumulative_lookups", clookups);
res.put("cumulative_hits", chits);
res.put("cumulative_hitratio", calcHitRatio(clookups, chits));
res.put("cumulative_inserts", stats.inserts.longValue());
res.put("cumulative_evictions", stats.evictions.longValue());
if (maxRamBytes != Long.MAX_VALUE) {
res.put("cumulative_evictionsRamUsage", stats.evictionsRamUsage.longValue());
}
});
manager.registerGauge(this, registryName, cacheMap, true, scope, getCategory().toString());
}
// for unit tests only
MetricsMap getMetricsMap() {
return cacheMap;
}
@Override
public MetricRegistry getMetricRegistry() {
return registry;
}
@Override
public String toString() {
return name() + (cacheMap != null ? cacheMap.getValue().toString() : "");
}
@Override
public long ramBytesUsed() {
synchronized (map) {
return BASE_RAM_BYTES_USED + ramBytesUsed;
}
}
@Override
public Collection<Accountable> getChildResources() {
if (maxRamBytes != Long.MAX_VALUE) {
synchronized (map) {
return Accountables.namedAccountables(getName(), (Map<?, ? extends Accountable>) map);
}
} else {
return Collections.emptyList();
}
}
}