/* * Licensed to ElasticSearch and Shay Banon 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.index.cache.filter.weighted; import com.google.common.cache.Cache; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.cache.Weigher; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.SegmentReader; import org.apache.lucene.search.DocIdSet; import org.apache.lucene.search.Filter; import org.elasticsearch.ElasticSearchException; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.lucene.docset.DocSet; import org.elasticsearch.common.lucene.docset.DocSets; import org.elasticsearch.common.lucene.search.NoCacheFilter; import org.elasticsearch.common.metrics.CounterMetric; import org.elasticsearch.common.metrics.MeanMetric; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.index.AbstractIndexComponent; import org.elasticsearch.index.Index; import org.elasticsearch.index.cache.filter.FilterCache; import org.elasticsearch.index.cache.filter.support.CacheKeyFilter; import org.elasticsearch.index.settings.IndexSettings; import org.elasticsearch.indices.cache.filter.IndicesFilterCache; import java.io.IOException; import java.util.concurrent.ConcurrentMap; public class WeightedFilterCache extends AbstractIndexComponent implements FilterCache, SegmentReader.CoreClosedListener, RemovalListener<WeightedFilterCache.FilterCacheKey, DocSet> { final IndicesFilterCache indicesFilterCache; final ConcurrentMap<Object, Boolean> seenReaders = ConcurrentCollections.newConcurrentMap(); final CounterMetric seenReadersCount = new CounterMetric(); final CounterMetric evictionsMetric = new CounterMetric(); final MeanMetric totalMetric = new MeanMetric(); @Inject public WeightedFilterCache(Index index, @IndexSettings Settings indexSettings, IndicesFilterCache indicesFilterCache) { super(index, indexSettings); this.indicesFilterCache = indicesFilterCache; indicesFilterCache.addRemovalListener(index.name(), this); } @Override public String type() { return "weighted"; } @Override public void close() throws ElasticSearchException { clear("close"); indicesFilterCache.removeRemovalListener(index.name()); } @Override public void clear(String reason) { logger.debug("full cache clear, reason [{}]", reason); for (Object readerKey : seenReaders.keySet()) { Boolean removed = seenReaders.remove(readerKey); if (removed == null) { return; } seenReadersCount.dec(); indicesFilterCache.addReaderKeyToClean(readerKey); } } @Override public void onClose(SegmentReader owner) { clear(owner); } @Override public void clear(IndexReader reader) { // we add the seen reader before we add the first cache entry for this reader // so, if we don't see it here, its won't be in the cache Boolean removed = seenReaders.remove(reader.getCoreCacheKey()); if (removed == null) { return; } seenReadersCount.dec(); indicesFilterCache.addReaderKeyToClean(reader.getCoreCacheKey()); } @Override public EntriesStats entriesStats() { long seenReadersCount = this.seenReadersCount.count(); return new EntriesStats(totalMetric.sum(), seenReadersCount == 0 ? 0 : totalMetric.count() / seenReadersCount); } @Override public long evictions() { return evictionsMetric.count(); } @Override public Filter cache(Filter filterToCache) { if (filterToCache instanceof NoCacheFilter) { return filterToCache; } if (isCached(filterToCache)) { return filterToCache; } return new FilterCacheFilterWrapper(filterToCache, this); } @Override public boolean isCached(Filter filter) { return filter instanceof FilterCacheFilterWrapper; } static class FilterCacheFilterWrapper extends Filter { private final Filter filter; private final WeightedFilterCache cache; FilterCacheFilterWrapper(Filter filter, WeightedFilterCache cache) { this.filter = filter; this.cache = cache; } @Override public DocIdSet getDocIdSet(IndexReader reader) throws IOException { Object filterKey = filter; if (filter instanceof CacheKeyFilter) { filterKey = ((CacheKeyFilter) filter).cacheKey(); } FilterCacheKey cacheKey = new FilterCacheKey(cache.index().name(), reader.getCoreCacheKey(), filterKey); Cache<FilterCacheKey, DocSet> innerCache = cache.indicesFilterCache.cache(); DocSet cacheValue = innerCache.getIfPresent(cacheKey); if (cacheValue == null) { if (!cache.seenReaders.containsKey(reader.getCoreCacheKey())) { Boolean previous = cache.seenReaders.putIfAbsent(reader.getCoreCacheKey(), Boolean.TRUE); if (previous == null && (reader instanceof SegmentReader)) { ((SegmentReader) reader).addCoreClosedListener(cache); cache.seenReadersCount.inc(); } } cacheValue = DocSets.cacheable(reader, filter.getDocIdSet(reader)); // we might put the same one concurrently, that's fine, it will be replaced and the removal // will be called cache.totalMetric.inc(cacheValue.sizeInBytes()); innerCache.put(cacheKey, cacheValue); } // return null if its EMPTY, this allows for further optimizations to ignore filters return cacheValue == DocSet.EMPTY_DOC_SET ? null : cacheValue; } public String toString() { return "cache(" + filter + ")"; } public boolean equals(Object o) { if (!(o instanceof FilterCacheFilterWrapper)) return false; return this.filter.equals(((FilterCacheFilterWrapper) o).filter); } public int hashCode() { return filter.hashCode() ^ 0x1117BF25; } } public static class FilterCacheValueWeigher implements Weigher<WeightedFilterCache.FilterCacheKey, DocSet> { @Override public int weigh(FilterCacheKey key, DocSet value) { int weight = (int) Math.min(value.sizeInBytes(), Integer.MAX_VALUE); return weight == 0 ? 1 : weight; } } // this will only be called for our index / data, IndicesFilterCache makes sure it works like this based on the // index we register the listener with @Override public void onRemoval(RemovalNotification<FilterCacheKey, DocSet> removalNotification) { if (removalNotification.wasEvicted()) { evictionsMetric.inc(); } if (removalNotification.getValue() != null) { totalMetric.dec(removalNotification.getValue().sizeInBytes()); } } public static class FilterCacheKey { private final String index; private final Object readerKey; private final Object filterKey; public FilterCacheKey(String index, Object readerKey, Object filterKey) { this.index = index; this.readerKey = readerKey; this.filterKey = filterKey; } public String index() { return index; } public Object readerKey() { return readerKey; } public Object filterKey() { return filterKey; } @Override public boolean equals(Object o) { if (this == o) return true; // if (o == null || getClass() != o.getClass()) return false; FilterCacheKey that = (FilterCacheKey) o; return (readerKey.equals(that.readerKey) && filterKey.equals(that.filterKey)); } @Override public int hashCode() { return readerKey.hashCode() + 31 * filterKey.hashCode(); } } }