/*
* 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.indices.cache.filter;
import com.google.common.base.Objects;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.google.common.collect.ImmutableMap;
import gnu.trove.set.hash.THashSet;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.common.CacheRecycler;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.lucene.docset.DocSet;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
import org.elasticsearch.index.cache.filter.weighted.WeightedFilterCache;
import org.elasticsearch.monitor.jvm.JvmInfo;
import org.elasticsearch.node.settings.NodeSettingsService;
import org.elasticsearch.threadpool.ThreadPool;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class IndicesFilterCache extends AbstractComponent implements RemovalListener<WeightedFilterCache.FilterCacheKey, DocSet> {
private final ThreadPool threadPool;
private Cache<WeightedFilterCache.FilterCacheKey, DocSet> cache;
private volatile String size;
private volatile long sizeInBytes;
private volatile TimeValue expire;
private final TimeValue cleanInterval;
private final Set<Object> readersKeysToClean = ConcurrentCollections.newConcurrentSet();
private volatile boolean closed;
private volatile Map<String, RemovalListener<WeightedFilterCache.FilterCacheKey, DocSet>> removalListeners =
ImmutableMap.of();
static {
MetaData.addDynamicSettings(
"indices.cache.filter.size",
"indices.cache.filter.expire"
);
}
class ApplySettings implements NodeSettingsService.Listener {
@Override
public void onRefreshSettings(Settings settings) {
boolean replace = false;
String size = settings.get("indices.cache.filter.size", IndicesFilterCache.this.size);
if (!size.equals(IndicesFilterCache.this.size)) {
logger.info("updating [indices.cache.filter.size] from [{}] to [{}]", IndicesFilterCache.this.size, size);
IndicesFilterCache.this.size = size;
replace = true;
}
TimeValue expire = settings.getAsTime("indices.cache.filter.expire", IndicesFilterCache.this.expire);
if (!Objects.equal(expire, IndicesFilterCache.this.expire)) {
logger.info("updating [indices.cache.filter.expire] from [{}] to [{}]", IndicesFilterCache.this.expire, expire);
IndicesFilterCache.this.expire = expire;
replace = true;
}
if (replace) {
Cache<WeightedFilterCache.FilterCacheKey, DocSet> oldCache = IndicesFilterCache.this.cache;
computeSizeInBytes();
buildCache();
oldCache.invalidateAll();
}
}
}
@Inject
public IndicesFilterCache(Settings settings, ThreadPool threadPool, NodeSettingsService nodeSettingsService) {
super(settings);
this.threadPool = threadPool;
this.size = componentSettings.get("size", "20%");
this.expire = componentSettings.getAsTime("expire", null);
this.cleanInterval = componentSettings.getAsTime("clean_interval", TimeValue.timeValueSeconds(60));
computeSizeInBytes();
buildCache();
logger.debug("using [node] weighted filter cache with size [{}], actual_size [{}], expire [{}], clean_interval [{}]",
size, new ByteSizeValue(sizeInBytes), expire, cleanInterval);
nodeSettingsService.addListener(new ApplySettings());
threadPool.schedule(cleanInterval, ThreadPool.Names.SAME, new ReaderCleaner());
}
private void buildCache() {
CacheBuilder<WeightedFilterCache.FilterCacheKey, DocSet> cacheBuilder = CacheBuilder.newBuilder()
.removalListener(this)
.maximumWeight(sizeInBytes).weigher(new WeightedFilterCache.FilterCacheValueWeigher());
// defaults to 4, but this is a busy map for all indices, increase it a bit
cacheBuilder.concurrencyLevel(16);
if (expire != null) {
cacheBuilder.expireAfterAccess(expire.millis(), TimeUnit.MILLISECONDS);
}
cache = cacheBuilder.build();
}
private void computeSizeInBytes() {
if (size.endsWith("%")) {
double percent = Double.parseDouble(size.substring(0, size.length() - 1));
sizeInBytes = (long) ((percent / 100) * JvmInfo.jvmInfo().getMem().getHeapMax().bytes());
} else {
sizeInBytes = ByteSizeValue.parseBytesSizeValue(size).bytes();
}
}
public synchronized void addRemovalListener(String index, RemovalListener<WeightedFilterCache.FilterCacheKey, DocSet> listener) {
removalListeners = MapBuilder.newMapBuilder(removalListeners).put(index, listener).immutableMap();
}
public synchronized void removeRemovalListener(String index) {
removalListeners = MapBuilder.newMapBuilder(removalListeners).remove(index).immutableMap();
}
public void addReaderKeyToClean(Object readerKey) {
readersKeysToClean.add(readerKey);
}
public void close() {
closed = true;
cache.invalidateAll();
}
public Cache<WeightedFilterCache.FilterCacheKey, DocSet> cache() {
return this.cache;
}
@Override
public void onRemoval(RemovalNotification<WeightedFilterCache.FilterCacheKey, DocSet> removalNotification) {
WeightedFilterCache.FilterCacheKey key = removalNotification.getKey();
if (key == null) {
return;
}
RemovalListener<WeightedFilterCache.FilterCacheKey, DocSet> listener = removalListeners.get(key.index());
if (listener != null) {
listener.onRemoval(removalNotification);
}
}
/**
* The reason we need this class ie because we need to clean all the filters that are associated
* with a reader. We don't want to do it every time a reader closes, since iterating over all the map
* is expensive. There doesn't seem to be a nicer way to do it (and maintaining a list per reader
* of the filters will cost more).
*/
class ReaderCleaner implements Runnable {
@Override
public void run() {
if (closed) {
return;
}
if (readersKeysToClean.isEmpty()) {
threadPool.schedule(cleanInterval, ThreadPool.Names.SAME, this);
return;
}
threadPool.executor(ThreadPool.Names.GENERIC).execute(new Runnable() {
@Override
public void run() {
THashSet<Object> keys = CacheRecycler.popHashSet();
try {
for (Iterator<Object> it = readersKeysToClean.iterator(); it.hasNext(); ) {
keys.add(it.next());
it.remove();
}
cache.cleanUp();
if (!keys.isEmpty()) {
for (Iterator<WeightedFilterCache.FilterCacheKey> it = cache.asMap().keySet().iterator(); it.hasNext(); ) {
WeightedFilterCache.FilterCacheKey filterCacheKey = it.next();
if (keys.contains(filterCacheKey.readerKey())) {
// same as invalidate
it.remove();
}
}
}
threadPool.schedule(cleanInterval, ThreadPool.Names.SAME, ReaderCleaner.this);
} finally {
CacheRecycler.pushHashSet(keys);
}
}
});
}
}
}