/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.elasticsearch.indices;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BulkScorer;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.LRUQueryCache;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryCache;
import org.apache.lucene.search.QueryCachingPolicy;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.ScorerSupplier;
import org.apache.lucene.search.Weight;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.lucene.ShardCoreKeyMap;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.index.cache.query.QueryCacheStats;
import org.elasticsearch.index.shard.ShardId;
import java.io.Closeable;
import java.io.IOException;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
public class IndicesQueryCache extends AbstractComponent implements QueryCache, Closeable {
public static final Setting<ByteSizeValue> INDICES_CACHE_QUERY_SIZE_SETTING =
Setting.memorySizeSetting("indices.queries.cache.size", "10%", Property.NodeScope);
public static final Setting<Integer> INDICES_CACHE_QUERY_COUNT_SETTING =
Setting.intSetting("indices.queries.cache.count", 10000, 1, Property.NodeScope);
// enables caching on all segments instead of only the larger ones, for testing only
public static final Setting<Boolean> INDICES_QUERIES_CACHE_ALL_SEGMENTS_SETTING =
Setting.boolSetting("indices.queries.cache.all_segments", false, Property.NodeScope);
private final LRUQueryCache cache;
private final ShardCoreKeyMap shardKeyMap = new ShardCoreKeyMap();
private final Map<ShardId, Stats> shardStats = new ConcurrentHashMap<>();
private volatile long sharedRamBytesUsed;
// This is a hack for the fact that the close listener for the
// ShardCoreKeyMap will be called before onDocIdSetEviction
// See onDocIdSetEviction for more info
private final Map<Object, StatsAndCount> stats2 = new IdentityHashMap<>();
public IndicesQueryCache(Settings settings) {
super(settings);
final ByteSizeValue size = INDICES_CACHE_QUERY_SIZE_SETTING.get(settings);
final int count = INDICES_CACHE_QUERY_COUNT_SETTING.get(settings);
logger.debug("using [node] query cache with size [{}] max filter count [{}]",
size, count);
if (INDICES_QUERIES_CACHE_ALL_SEGMENTS_SETTING.get(settings)) {
cache = new ElasticsearchLRUQueryCache(count, size.getBytes(), context -> true);
} else {
cache = new ElasticsearchLRUQueryCache(count, size.getBytes());
}
sharedRamBytesUsed = 0;
}
/** Get usage statistics for the given shard. */
public QueryCacheStats getStats(ShardId shard) {
final Map<ShardId, QueryCacheStats> stats = new HashMap<>();
for (Map.Entry<ShardId, Stats> entry : shardStats.entrySet()) {
stats.put(entry.getKey(), entry.getValue().toQueryCacheStats());
}
QueryCacheStats shardStats = new QueryCacheStats();
QueryCacheStats info = stats.get(shard);
if (info == null) {
info = new QueryCacheStats();
}
shardStats.add(info);
// We also have some shared ram usage that we try to distribute to
// proportionally to their number of cache entries of each shard
long totalSize = 0;
for (QueryCacheStats s : stats.values()) {
totalSize += s.getCacheSize();
}
final double weight = totalSize == 0
? 1d / stats.size()
: shardStats.getCacheSize() / totalSize;
final long additionalRamBytesUsed = Math.round(weight * sharedRamBytesUsed);
shardStats.add(new QueryCacheStats(additionalRamBytesUsed, 0, 0, 0, 0));
return shardStats;
}
@Override
public Weight doCache(Weight weight, QueryCachingPolicy policy) {
while (weight instanceof CachingWeightWrapper) {
weight = ((CachingWeightWrapper) weight).in;
}
final Weight in = cache.doCache(weight, policy);
// We wrap the weight to track the readers it sees and map them with
// the shards they belong to
return new CachingWeightWrapper(in);
}
private class CachingWeightWrapper extends Weight {
private final Weight in;
protected CachingWeightWrapper(Weight in) {
super(in.getQuery());
this.in = in;
}
@Override
public void extractTerms(Set<Term> terms) {
in.extractTerms(terms);
}
@Override
public Explanation explain(LeafReaderContext context, int doc) throws IOException {
shardKeyMap.add(context.reader());
return in.explain(context, doc);
}
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
shardKeyMap.add(context.reader());
return in.scorer(context);
}
@Override
public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
shardKeyMap.add(context.reader());
return in.scorerSupplier(context);
}
@Override
public BulkScorer bulkScorer(LeafReaderContext context) throws IOException {
shardKeyMap.add(context.reader());
return in.bulkScorer(context);
}
}
/** Clear all entries that belong to the given index. */
public void clearIndex(String index) {
final Set<Object> coreCacheKeys = shardKeyMap.getCoreKeysForIndex(index);
for (Object coreKey : coreCacheKeys) {
cache.clearCoreCacheKey(coreKey);
}
// This cache stores two things: filters, and doc id sets. Calling
// clear only removes the doc id sets, but if we reach the situation
// that the cache does not contain any DocIdSet anymore, then it
// probably means that the user wanted to remove everything.
if (cache.getCacheSize() == 0) {
cache.clear();
}
}
@Override
public void close() {
assert shardKeyMap.size() == 0 : shardKeyMap.size();
assert shardStats.isEmpty() : shardStats.keySet();
assert stats2.isEmpty() : stats2;
cache.clear();
}
private static class Stats implements Cloneable {
volatile long ramBytesUsed;
volatile long hitCount;
volatile long missCount;
volatile long cacheCount;
volatile long cacheSize;
QueryCacheStats toQueryCacheStats() {
return new QueryCacheStats(ramBytesUsed, hitCount, missCount, cacheCount, cacheSize);
}
}
private static class StatsAndCount {
int count;
final Stats stats;
StatsAndCount(Stats stats) {
this.stats = stats;
this.count = 0;
}
}
private boolean empty(Stats stats) {
if (stats == null) {
return true;
}
return stats.cacheSize == 0 && stats.ramBytesUsed == 0;
}
public void onClose(ShardId shardId) {
assert empty(shardStats.get(shardId));
shardStats.remove(shardId);
}
private class ElasticsearchLRUQueryCache extends LRUQueryCache {
ElasticsearchLRUQueryCache(int maxSize, long maxRamBytesUsed, Predicate<LeafReaderContext> leavesToCache) {
super(maxSize, maxRamBytesUsed, leavesToCache);
}
ElasticsearchLRUQueryCache(int maxSize, long maxRamBytesUsed) {
super(maxSize, maxRamBytesUsed);
}
private Stats getStats(Object coreKey) {
final ShardId shardId = shardKeyMap.getShardId(coreKey);
if (shardId == null) {
return null;
}
return shardStats.get(shardId);
}
private Stats getOrCreateStats(Object coreKey) {
final ShardId shardId = shardKeyMap.getShardId(coreKey);
Stats stats = shardStats.get(shardId);
if (stats == null) {
stats = new Stats();
shardStats.put(shardId, stats);
}
return stats;
}
// It's ok to not protect these callbacks by a lock since it is
// done in LRUQueryCache
@Override
protected void onClear() {
super.onClear();
for (Stats stats : shardStats.values()) {
// don't throw away hit/miss
stats.cacheSize = 0;
stats.ramBytesUsed = 0;
}
sharedRamBytesUsed = 0;
}
@Override
protected void onQueryCache(Query filter, long ramBytesUsed) {
super.onQueryCache(filter, ramBytesUsed);
sharedRamBytesUsed += ramBytesUsed;
}
@Override
protected void onQueryEviction(Query filter, long ramBytesUsed) {
super.onQueryEviction(filter, ramBytesUsed);
sharedRamBytesUsed -= ramBytesUsed;
}
@Override
protected void onDocIdSetCache(Object readerCoreKey, long ramBytesUsed) {
super.onDocIdSetCache(readerCoreKey, ramBytesUsed);
final Stats shardStats = getOrCreateStats(readerCoreKey);
shardStats.cacheSize += 1;
shardStats.cacheCount += 1;
shardStats.ramBytesUsed += ramBytesUsed;
StatsAndCount statsAndCount = stats2.get(readerCoreKey);
if (statsAndCount == null) {
statsAndCount = new StatsAndCount(shardStats);
stats2.put(readerCoreKey, statsAndCount);
}
statsAndCount.count += 1;
}
@Override
protected void onDocIdSetEviction(Object readerCoreKey, int numEntries, long sumRamBytesUsed) {
super.onDocIdSetEviction(readerCoreKey, numEntries, sumRamBytesUsed);
// onDocIdSetEviction might sometimes be called with a number
// of entries equal to zero if the cache for the given segment
// was already empty when the close listener was called
if (numEntries > 0) {
// We can't use ShardCoreKeyMap here because its core closed
// listener is called before the listener of the cache which
// triggers this eviction. So instead we use use stats2 that
// we only evict when nothing is cached anymore on the segment
// instead of relying on close listeners
final StatsAndCount statsAndCount = stats2.get(readerCoreKey);
final Stats shardStats = statsAndCount.stats;
shardStats.cacheSize -= numEntries;
shardStats.ramBytesUsed -= sumRamBytesUsed;
statsAndCount.count -= numEntries;
if (statsAndCount.count == 0) {
stats2.remove(readerCoreKey);
}
}
}
@Override
protected void onHit(Object readerCoreKey, Query filter) {
super.onHit(readerCoreKey, filter);
final Stats shardStats = getStats(readerCoreKey);
shardStats.hitCount += 1;
}
@Override
protected void onMiss(Object readerCoreKey, Query filter) {
super.onMiss(readerCoreKey, filter);
final Stats shardStats = getOrCreateStats(readerCoreKey);
shardStats.missCount += 1;
}
}
}