-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Closed
Description
We just discovered that while observing a lot of cache misses in our logs.
I isolated the problem down to simple unit test. However, it is intermittent, and is not reproducible if is run under gradle testing. So I put multiple attempts inside a loop and also recommend using built-in Eclipse tester to reproduce that. I am also able to reproduce as a simple app with main() on Linux and Mac.
The unit tests are provided below. There are two scenarios. One test that uses only one cache key works fine, another test that uses the same logic but with two keys fails on unexpected cache miss.
package com.thomsonreuters.onep.pito.utils;
import static org.junit.Assert.assertEquals;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
public class CaffeineTest {
private static final int ATTEMPTS = 100;
private static final int TTL = 100;
private static final int EPSILON = 10;
@Test
public void testWithCoupleOfKeys() throws Exception {
testExpiration(true);
}
@Test
public void testSameWithSingleKey() throws Exception {
testExpiration(false);
}
private void testExpiration(boolean withPairOfKey) throws Exception {
final ConcurrentMap<String,String> source = new ConcurrentHashMap<>();
final ConcurrentMap<String,Date> lastLoad = new ConcurrentHashMap<>();
class Loader implements CacheLoader<String,String> {
private void reportCacheMiss(String key) {
Date now = new Date();
Date last = lastLoad.get(key);
System.out.print(new SimpleDateFormat("hh:MM:ss.SSS").format(new Date()) + ": cache miss for key: " + key);
if (last != null) {
System.out.println(" - " + (now.getTime() - last.getTime()) + "ms after last load");
} else {
System.out.println();
}
lastLoad.put(key, now);
}
@Override
public String load(String key) {
reportCacheMiss(key);
return source.get(key);
}
@Override
public CompletableFuture<String> asyncLoad(String key, Executor executor) {
reportCacheMiss(key);
return CompletableFuture.completedFuture(source.get(key));
}
}
for (int i = 0; i < ATTEMPTS; i++) {
System.out.println("Attempt #" + i);
//initial value
source.put("foo", "foo0");
if (withPairOfKey) {
source.put("bar", "bar0");
}
lastLoad.clear();
AsyncLoadingCache<String,String> cache = Caffeine.newBuilder()
.expireAfterWrite(TTL, TimeUnit.MILLISECONDS)
.buildAsync(new Loader());
assertEquals("should serve initial value", "foo0", cache.get("foo").get());
if (withPairOfKey) {
assertEquals("should serve initial value", "bar0", cache.get("bar").get());
}
//first update
source.put("foo", "foo1");
if (withPairOfKey) {
source.put("bar", "bar1");
}
assertEquals("should serve cached initial value", "foo0", cache.get("foo").get());
if (withPairOfKey) {
assertEquals("should serve cached initial value", "bar0", cache.get("bar").get());
}
Thread.sleep(EPSILON); //sleep for less than expiration
assertEquals("should still serve cached initial value", "foo0", cache.get("foo").get());
if (withPairOfKey) {
assertEquals("should still serve cached initial value", "bar0", cache.get("bar").get());
}
Thread.sleep(TTL + EPSILON); //sleep until expiration
assertEquals("should now serve first updated value", "foo1", cache.get("foo").get());
if (withPairOfKey) {
assertEquals("should now serve first updated value", "bar1", cache.get("bar").get());
}
//second update
source.put("foo", "foo2");
if (withPairOfKey) {
source.put("bar", "bar2");
}
assertEquals("should still serve cached first updated value", "foo1", cache.get("foo").get());
if (withPairOfKey) {
assertEquals("should still serve cached first updated value", "bar1", cache.get("bar").get());
}
Thread.sleep(EPSILON); //sleep for less than expiration
//it never fails here for the first key
assertEquals("should still serve cached first updated value", "foo1", cache.get("foo").get());
if (withPairOfKey) {
//it fails here after a few attempts because of cache miss
assertEquals("should still serve cached first updated value", "bar1", cache.get("bar").get());
}
}
}
}Metadata
Metadata
Assignees
Labels
No labels