blob: 27ecf9ce5eede12c8f02b9dbbda93144fca794f6 [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.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import com.github.benmanes.caffeine.cache.RemovalCause;
import org.apache.lucene.util.Accountable;
import org.apache.lucene.util.TestUtil;
import org.apache.solr.SolrTestCase;
import org.apache.solr.metrics.SolrMetricManager;
import org.junit.Test;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
/**
* Test for {@link CaffeineCache}.
*/
public class TestCaffeineCache extends SolrTestCase {
SolrMetricManager metricManager = new SolrMetricManager();
String registry = TestUtil.randomSimpleString(random(), 2, 10);
String scope = TestUtil.randomSimpleString(random(), 2, 10);
@Test
public void testSimple() throws IOException {
CaffeineCache<Integer, String> lfuCache = new CaffeineCache<>();
lfuCache.initializeMetrics(metricManager, registry, "foo", scope + "-1");
CaffeineCache<Integer, String> newLFUCache = new CaffeineCache<>();
newLFUCache.initializeMetrics(metricManager, registry, "foo2", scope + "-2");
Map<String, String> params = new HashMap<>();
params.put("size", "100");
params.put("initialSize", "10");
params.put("autowarmCount", "25");
NoOpRegenerator regenerator = new NoOpRegenerator();
Object initObj = lfuCache.init(params, null, regenerator);
lfuCache.setState(SolrCache.State.LIVE);
for (int i = 0; i < 101; i++) {
lfuCache.put(i + 1, Integer.toString(i + 1));
}
assertEquals("15", lfuCache.get(15));
assertEquals("75", lfuCache.get(75));
assertEquals(null, lfuCache.get(110));
Map<String, Object> nl = lfuCache.getMetricsMap().getValue();
assertEquals(3L, nl.get("lookups"));
assertEquals(2L, nl.get("hits"));
assertEquals(101L, nl.get("inserts"));
assertEquals(null, lfuCache.get(1)); // first item put in should be the first out
// Test autowarming
newLFUCache.init(params, initObj, regenerator);
newLFUCache.warm(null, lfuCache);
newLFUCache.setState(SolrCache.State.LIVE);
newLFUCache.put(103, "103");
assertEquals("15", newLFUCache.get(15));
assertEquals("75", newLFUCache.get(75));
assertEquals(null, newLFUCache.get(50));
nl = newLFUCache.getMetricsMap().getValue();
assertEquals(3L, nl.get("lookups"));
assertEquals(2L, nl.get("hits"));
assertEquals(1L, nl.get("inserts"));
assertEquals(0L, nl.get("evictions"));
assertEquals(7L, nl.get("cumulative_lookups"));
assertEquals(4L, nl.get("cumulative_hits"));
assertEquals(102L, nl.get("cumulative_inserts"));
}
@Test
public void testTimeDecay() {
Cache<Integer, String> cacheDecay = Caffeine.newBuilder()
.executor(Runnable::run)
.maximumSize(20)
.build();
for (int i = 1; i < 21; i++) {
cacheDecay.put(i, Integer.toString(i));
}
Map<Integer, String> itemsDecay;
// Now increase the freq count for 5 items
for (int i = 0; i < 5; ++i) {
for (int j = 0; j < 10; ++j) {
cacheDecay.getIfPresent(i + 13);
}
}
// OK, 13 - 17 should have larger counts and should stick past next few collections
cacheDecay.put(22, "22");
cacheDecay.put(23, "23");
cacheDecay.put(24, "24");
cacheDecay.put(25, "25");
itemsDecay = cacheDecay.policy().eviction().get().hottest(10);
// 13 - 17 should be in cache, but 11 and 18 (among others) should not. Testing that elements before and
// after the ones with increased counts are removed, and all the increased count ones are still in the cache
assertNull(itemsDecay.get(11));
assertNull(itemsDecay.get(18));
assertNotNull(itemsDecay.get(13));
assertNotNull(itemsDecay.get(14));
assertNotNull(itemsDecay.get(15));
assertNotNull(itemsDecay.get(16));
assertNotNull(itemsDecay.get(17));
// Testing that all the elements in front of the ones with increased counts are gone
for (int idx = 26; idx < 32; ++idx) {
cacheDecay.put(idx, Integer.toString(idx));
}
//Surplus count should be at 0
itemsDecay = cacheDecay.policy().eviction().get().hottest(10);
assertNull(itemsDecay.get(20));
assertNull(itemsDecay.get(24));
assertNotNull(itemsDecay.get(13));
assertNotNull(itemsDecay.get(14));
assertNotNull(itemsDecay.get(15));
assertNotNull(itemsDecay.get(16));
assertNotNull(itemsDecay.get(17));
}
@Test
public void testMaxIdleTime() throws Exception {
int IDLE_TIME_SEC = 5;
CountDownLatch removed = new CountDownLatch(1);
AtomicReference<RemovalCause> removalCause = new AtomicReference<>();
CaffeineCache<String, String> cache = new CaffeineCache<String, String>() {
@Override
public void onRemoval(String key, String value, RemovalCause cause) {
super.onRemoval(key, value, cause);
removalCause.set(cause);
removed.countDown();
}
};
Map<String, String> params = new HashMap<>();
params.put("size", "6");
params.put("maxIdleTime", "" + IDLE_TIME_SEC);
cache.init(params, null, new NoOpRegenerator());
cache.put("foo", "bar");
assertEquals("bar", cache.get("foo"));
// sleep for at least the idle time before inserting other entries
// the eviction is piggy-backed on put()
Thread.sleep(TimeUnit.SECONDS.toMillis(IDLE_TIME_SEC * 2));
cache.put("abc", "xyz");
boolean await = removed.await(30, TimeUnit.SECONDS);
assertTrue("did not expire entry in in time", await);
assertEquals(RemovalCause.EXPIRED, removalCause.get());
assertNull(cache.get("foo"));
}
@Test
public void testSetLimits() throws Exception {
AtomicReference<CountDownLatch> removed = new AtomicReference<>(new CountDownLatch(2));
List<RemovalCause> removalCauses = new ArrayList<>();
List<String> removedKeys = new ArrayList<>();
Set<String> allKeys = new HashSet<>();
CaffeineCache<String, Accountable> cache = new CaffeineCache<String, Accountable>() {
@Override
public Accountable put(String key, Accountable val) {
allKeys.add(key);
return super.put(key, val);
}
@Override
public void onRemoval(String key, Accountable value, RemovalCause cause) {
super.onRemoval(key, value, cause);
removalCauses.add(cause);
removedKeys.add(key);
removed.get().countDown();
}
};
Map<String, String> params = new HashMap<>();
params.put("size", "5");
cache.init(params, null, new NoOpRegenerator());
for (int i = 0; i < 5; i++) {
cache.put("foo-" + i, new Accountable() {
@Override
public long ramBytesUsed() {
return 1024 * 1024;
}
});
}
assertEquals(5, cache.size());
// no evictions yet
assertEquals(2, removed.get().getCount());
cache.put("abc1", new Accountable() {
@Override
public long ramBytesUsed() {
return 1;
}
});
cache.put("abc2", new Accountable() {
@Override
public long ramBytesUsed() {
return 2;
}
});
boolean await = removed.get().await(30, TimeUnit.SECONDS);
assertTrue("did not evict entries in in time", await);
assertEquals(5, cache.size());
assertEquals(2, cache.get("abc2").ramBytesUsed());
for (String key : removedKeys) {
assertNull("key " + key + " still present!", cache.get(key));
allKeys.remove(key);
}
for (RemovalCause cause : removalCauses) {
assertEquals(RemovalCause.SIZE, cause);
}
removed.set(new CountDownLatch(2));
removalCauses.clear();
removedKeys.clear();
// trim down by item count
cache.setMaxSize(3);
cache.put("abc3", new Accountable() {
@Override
public long ramBytesUsed() {
return 3;
}
});
await = removed.get().await(30, TimeUnit.SECONDS);
assertTrue("did not evict entries in in time", await);
assertEquals(3, cache.size());
for (String key : removedKeys) {
assertNull("key " + key + " still present!", cache.get(key));
allKeys.remove(key);
}
for (RemovalCause cause : removalCauses) {
assertEquals(RemovalCause.SIZE, cause);
}
// at least one item has to go
removed.set(new CountDownLatch(1));
removalCauses.clear();
removedKeys.clear();
// trim down by ram size
cache.setMaxRamMB(1);
await = removed.get().await(30, TimeUnit.SECONDS);
assertTrue("did not evict entries in in time", await);
for (String key : removedKeys) {
assertNull("key " + key + " still present!", cache.get(key));
allKeys.remove(key);
}
for (RemovalCause cause : removalCauses) {
assertEquals(RemovalCause.SIZE, cause);
}
// check total size of remaining items
long total = 0;
for (String key : allKeys) {
Accountable a = cache.get(key);
assertNotNull("missing value for key " + key, a);
total += a.ramBytesUsed();
}
assertTrue("total ram bytes should be greater than 0", total > 0);
assertTrue("total ram bytes exceeded limit", total < 1024 * 1024);
cache.close();
}
}