blob: bf3b01049e0d0205606613912a5594a618e85c96 [file] [log] [blame]
/*
* 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.io.IOException;
import java.io.UncheckedIOException;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import org.apache.lucene.util.Accountable;
import org.apache.lucene.util.RamUsageEstimator;
import org.apache.solr.common.SolrException;
import org.apache.solr.metrics.MetricsMap;
import org.apache.solr.metrics.SolrMetricsContext;
import org.apache.solr.util.ConcurrentLRUCache;
import org.apache.solr.util.IOFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SolrCache based on ConcurrentLRUCache implementation.
* <p>
* This implementation does not use a separate cleanup thread. Instead it uses the calling thread
* itself to do the cleanup when the size of the cache exceeds certain limits.
* <p>
* Also see <a href="http://wiki.apache.org/solr/SolrCaching">SolrCaching</a>
*
* @see org.apache.solr.util.ConcurrentLRUCache
* @see org.apache.solr.search.SolrCache
* @since solr 1.4
* @deprecated This cache implementation is deprecated and will be removed in Solr 9.0.
* Use {@link CaffeineCache} instead.
*/
public class FastLRUCache<K, V> extends SolrCacheBase implements SolrCache<K, V>, Accountable {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(FastLRUCache.class);
public static final String MIN_SIZE_PARAM = "minSize";
public static final String ACCEPTABLE_SIZE_PARAM = "acceptableSize";
// contains the statistics objects for all open caches of the same type
private List<ConcurrentLRUCache.Stats> statsList;
private long warmupTime = 0;
private String description = "Concurrent LRU Cache";
private ConcurrentLRUCache<K, V> cache;
private int showItems = 0;
private long maxRamBytes;
private int maxSize;
private int minSizeLimit;
private int initialSize;
private int acceptableSize;
private boolean cleanupThread;
private int maxIdleTimeSec;
private long ramLowerWatermark;
private MetricsMap cacheMap;
private Set<String> metricNames = ConcurrentHashMap.newKeySet();
private SolrMetricsContext solrMetricsContext;
@Override
public Object init(Map args, Object persistence, CacheRegenerator regenerator) {
super.init(args, regenerator);
String str = (String) args.get(SIZE_PARAM);
maxSize = str == null ? 1024 : Integer.parseInt(str);
str = (String) args.get(MIN_SIZE_PARAM);
if (str == null) {
minSizeLimit = (int) (maxSize * 0.9);
} else {
minSizeLimit = Integer.parseInt(str);
}
checkAndAdjustLimits();
str = (String) args.get(ACCEPTABLE_SIZE_PARAM);
if (str == null) {
acceptableSize = (int) (maxSize * 0.95);
} else {
acceptableSize = Integer.parseInt(str);
}
// acceptable limit should be somewhere between minLimit and limit
acceptableSize = Math.max(minSizeLimit, acceptableSize);
str = (String) args.get(INITIAL_SIZE_PARAM);
initialSize = str == null ? maxSize : Integer.parseInt(str);
str = (String) args.get(CLEANUP_THREAD_PARAM);
cleanupThread = str == null ? false : Boolean.parseBoolean(str);
str = (String) args.get(SHOW_ITEMS_PARAM);
showItems = str == null ? 0 : Integer.parseInt(str);
str = (String) args.get(MAX_IDLE_TIME_PARAM);
if (str == null) {
maxIdleTimeSec = -1;
} else {
maxIdleTimeSec = Integer.parseInt(str);
}
str = (String) args.get(MAX_RAM_MB_PARAM);
long maxRamMB = str == null ? -1 : (long) Double.parseDouble(str);
this.maxRamBytes = maxRamMB < 0 ? Long.MAX_VALUE : maxRamMB * 1024L * 1024L;
if (maxRamBytes != Long.MAX_VALUE) {
ramLowerWatermark = Math.round(maxRamBytes * 0.8);
description = generateDescription(maxRamBytes, ramLowerWatermark, cleanupThread);
cache = new ConcurrentLRUCache<>(ramLowerWatermark, maxRamBytes, cleanupThread, null, maxIdleTimeSec);
} else {
ramLowerWatermark = -1L;
description = generateDescription(maxSize, initialSize, minSizeLimit, acceptableSize, cleanupThread);
cache = new ConcurrentLRUCache<>(maxSize, minSizeLimit, acceptableSize, initialSize, cleanupThread, false, null, maxIdleTimeSec);
}
cache.setAlive(false);
statsList = (List<ConcurrentLRUCache.Stats>) persistence;
if (statsList == null) {
// must be the first time a cache of this type is being created
// Use a CopyOnWriteArrayList since puts are very rare and iteration may be a frequent operation
// because it is used in getStatistics()
statsList = new CopyOnWriteArrayList<>();
// the first entry will be for cumulative stats of caches that have been closed.
statsList.add(new ConcurrentLRUCache.Stats());
}
statsList.add(cache.getStats());
cacheMap = new MetricsMap((detailed, map) -> {
if (cache != null) {
ConcurrentLRUCache.Stats stats = cache.getStats();
long lookups = stats.getCumulativeLookups();
long hits = stats.getCumulativeHits();
long inserts = stats.getCumulativePuts();
long evictions = stats.getCumulativeEvictions();
long idleEvictions = stats.getCumulativeIdleEvictions();
long size = stats.getCurrentSize();
long clookups = 0;
long chits = 0;
long cinserts = 0;
long cevictions = 0;
long cIdleEvictions = 0;
// NOTE: It is safe to iterate on a CopyOnWriteArrayList
for (ConcurrentLRUCache.Stats statistiscs : statsList) {
clookups += statistiscs.getCumulativeLookups();
chits += statistiscs.getCumulativeHits();
cinserts += statistiscs.getCumulativePuts();
cevictions += statistiscs.getCumulativeEvictions();
cIdleEvictions += statistiscs.getCumulativeIdleEvictions();
}
map.put(LOOKUPS_PARAM, lookups);
map.put(HITS_PARAM, hits);
map.put(HIT_RATIO_PARAM, calcHitRatio(lookups, hits));
map.put(INSERTS_PARAM, inserts);
map.put(EVICTIONS_PARAM, evictions);
map.put(SIZE_PARAM, size);
map.put("cleanupThread", cleanupThread);
map.put("idleEvictions", idleEvictions);
map.put(RAM_BYTES_USED_PARAM, ramBytesUsed());
map.put(MAX_RAM_MB_PARAM, getMaxRamMB());
map.put("warmupTime", warmupTime);
map.put("cumulative_lookups", clookups);
map.put("cumulative_hits", chits);
map.put("cumulative_hitratio", calcHitRatio(clookups, chits));
map.put("cumulative_inserts", cinserts);
map.put("cumulative_evictions", cevictions);
map.put("cumulative_idleEvictions", cIdleEvictions);
if (detailed && showItems != 0) {
Map items = cache.getLatestAccessedItems(showItems == -1 ? Integer.MAX_VALUE : showItems);
for (Map.Entry e : (Set<Map.Entry>) items.entrySet()) {
Object k = e.getKey();
Object v = e.getValue();
String ks = "item_" + k;
String vs = v.toString();
map.put(ks, vs);
}
}
}
});
return statsList;
}
protected String generateDescription() {
if (maxRamBytes != Long.MAX_VALUE) {
return generateDescription(maxRamBytes, ramLowerWatermark, cleanupThread);
} else {
return generateDescription(maxSize, initialSize, minSizeLimit, acceptableSize, cleanupThread);
}
}
/**
* @return Returns the description of this Cache.
*/
protected String generateDescription(int limit, int initialSize, int minLimit, int acceptableLimit, boolean newThread) {
String description = "Concurrent LRU Cache(maxSize=" + limit + ", initialSize=" + initialSize +
", minSize=" + minLimit + ", acceptableSize=" + acceptableLimit + ", cleanupThread=" + newThread;
if (isAutowarmingOn()) {
description += ", " + getAutowarmDescription();
}
description += ')';
return description;
}
protected String generateDescription(long maxRamBytes, long ramLowerWatermark, boolean newThread) {
String description = "Concurrent LRU Cache(ramMinSize=" + ramLowerWatermark + ", ramMaxSize=" + maxRamBytes
+ ", cleanupThread=" + newThread;
if (isAutowarmingOn()) {
description += ", " + getAutowarmDescription();
}
description += ')';
return description;
}
@Override
public int size() {
return cache.size();
}
@Override
public V put(K key, V value) {
return cache.put(key, value);
}
@Override
public V remove(K key) {
return cache.remove(key);
}
@Override
public V get(K key) {
return cache.get(key);
}
@Override
public V computeIfAbsent(K key, IOFunction<? super K, ? extends V> mappingFunction) {
return cache.computeIfAbsent(key, k -> {
try {
return mappingFunction.apply(k);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
@Override
public void clear() {
cache.clear();
}
@Override
public void setState(State state) {
super.setState(state);
cache.setAlive(state == State.LIVE);
}
@Override
public void warm(SolrIndexSearcher searcher, SolrCache old) {
if (regenerator == null) return;
long warmingStartTime = System.nanoTime();
FastLRUCache other = (FastLRUCache) old;
// warm entries
if (isAutowarmingOn()) {
int sz = autowarm.getWarmCount(other.size());
Map items = other.cache.getLatestAccessedItems(sz);
Map.Entry[] itemsArr = new Map.Entry[items.size()];
int counter = 0;
for (Object mapEntry : items.entrySet()) {
itemsArr[counter++] = (Map.Entry) mapEntry;
}
for (int i = itemsArr.length - 1; i >= 0; i--) {
try {
boolean continueRegen = regenerator.regenerateItem(searcher,
this, old, itemsArr[i].getKey(), itemsArr[i].getValue());
if (!continueRegen) break;
} catch (Exception e) {
SolrException.log(log, "Error during auto-warming of key:" + itemsArr[i].getKey(), e);
}
}
}
warmupTime = TimeUnit.MILLISECONDS.convert(System.nanoTime() - warmingStartTime, TimeUnit.NANOSECONDS);
}
@Override
public void close() throws IOException {
SolrCache.super.close();
// add the stats to the cumulative stats object (the first in the statsList)
statsList.get(0).add(cache.getStats());
statsList.remove(cache.getStats());
cache.destroy();
}
//////////////////////// SolrInfoMBeans methods //////////////////////
@Override
public String getName() {
return FastLRUCache.class.getName();
}
@Override
public String getDescription() {
return description;
}
@Override
public Set<String> getMetricNames() {
return metricNames;
}
@Override
public SolrMetricsContext getSolrMetricsContext() {
return solrMetricsContext;
}
@Override
public void initializeMetrics(SolrMetricsContext parentContext, String scope) {
this.solrMetricsContext = parentContext.getChildContext(this);
this.solrMetricsContext.gauge(this, cacheMap, true, scope, getCategory().toString());
}
// for unit tests only
MetricsMap getMetricsMap() {
return cacheMap;
}
@Override
public String toString() {
return name() + (cacheMap != null ? cacheMap.getValue().toString() : "");
}
@Override
public long ramBytesUsed() {
return BASE_RAM_BYTES_USED +
RamUsageEstimator.sizeOfObject(cache) +
RamUsageEstimator.sizeOfObject(statsList);
}
@Override
public int getMaxSize() {
return maxSize != Integer.MAX_VALUE ? maxSize : -1;
}
@Override
public void setMaxSize(int maxSize) {
if (maxSize > 0) {
this.maxSize = maxSize;
} else {
this.maxSize = Integer.MAX_VALUE;
}
checkAndAdjustLimits();
cache.setUpperWaterMark(maxSize);
cache.setLowerWaterMark(minSizeLimit);
description = generateDescription();
}
@Override
public int getMaxRamMB() {
return maxRamBytes != Long.MAX_VALUE ? (int) (maxRamBytes / 1024L / 1024L) : -1;
}
@Override
public void setMaxRamMB(int maxRamMB) {
maxRamBytes = maxRamMB < 0 ? Long.MAX_VALUE : maxRamMB * 1024L * 1024L;
if (maxRamMB < 0) {
ramLowerWatermark = Long.MIN_VALUE;
} else {
ramLowerWatermark = Math.round(maxRamBytes * 0.8);
}
cache.setRamUpperWatermark(maxRamBytes);
cache.setRamLowerWatermark(ramLowerWatermark);
description = generateDescription();
}
private void checkAndAdjustLimits() {
if (minSizeLimit <= 0) minSizeLimit = 1;
if (maxSize <= minSizeLimit) {
if (maxSize > 1) {
minSizeLimit = maxSize - 1;
} else {
maxSize = minSizeLimit + 1;
}
}
}
}