use guava's cache as implementation for the HotEntryCache

My own implementation was faster, but was not able to
implement a size limitation.
This commit is contained in:
2019-02-16 10:23:52 +01:00
parent 7b00eede86
commit 117ef4ea34
5 changed files with 354 additions and 733 deletions

View File

@@ -1,301 +1,301 @@
package org.lucares.utils.cache;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.config.Configurator;
import org.lucares.utils.cache.HotEntryCache.EventType;
import org.testng.Assert;
import org.testng.annotations.Test;
@Test
public class HotEntryCacheTest {
public void testPutAndGet() throws InterruptedException, ExecutionException, TimeoutException {
final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
final String replacedNull = cache.put("key", "value1");
Assert.assertEquals(replacedNull, null);
final String cachedValue1 = cache.get("key");
Assert.assertEquals(cachedValue1, "value1");
final String replacedValue1 = cache.put("key", "value2");
Assert.assertEquals(replacedValue1, "value1");
final String cachedValue2 = cache.get("key");
Assert.assertEquals(cachedValue2, "value2");
}
public void testPutTouches() throws InterruptedException, ExecutionException, TimeoutException {
final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock();
final Duration timeToLive = Duration.ofSeconds(10);
final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive, clock);
cache.put("key", "value1");
clock.plusSeconds(2);
cache.updateTime();
cache.put("key", "value2");
clock.plus(timeToLive.minusSeconds(1));
cache.triggerEvictionAndWait();
// at this point the entry would have been evicted it it was not touched by the
// second put.
final String cachedValue2 = cache.get("key");
Assert.assertEquals(cachedValue2, "value2");
clock.plus(timeToLive.plusSeconds(1));
// time elapsed since the last put: timeToLive +1s
cache.triggerEvictionAndWait();
final String cachedValue1_evicted = cache.get("key");
Assert.assertEquals(cachedValue1_evicted, null);
}
public void testGetTouches() throws Exception {
final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock();
final Duration timeToLive = Duration.ofSeconds(10);
final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive, clock);
cache.put("key", "value1");
// skip forward in time, but do not yet trigger eviction
clock.plus(timeToLive.plusMillis(1));
cache.updateTime();
cache.get("key"); // will touch the entry
cache.triggerEvictionAndWait(); // if get didn't touch, then this will evict the entry
final String cachedValue1 = cache.get("key");
Assert.assertEquals(cachedValue1, "value1");
}
public void testEvictionByBackgroundThread() throws InterruptedException, ExecutionException, TimeoutException {
final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock();
final Duration timeToLive = Duration.ofSeconds(10);
final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive, clock);
final CompletableFuture<String> evictionEventFuture = new CompletableFuture<>();
cache.addListener(event -> {
evictionEventFuture.complete(event.getValue());
}, EventType.EVICTED);
cache.put("key", "value1");
clock.plus(timeToLive.minusSeconds(1));
cache.updateTime();
cache.put("key2", "value2");
clock.plus(Duration.ofSeconds(1).plusMillis(1));
cache.triggerEvictionAndWait();
final String evictedValue1 = evictionEventFuture.get(5, TimeUnit.MINUTES); // enough time for debugging
Assert.assertEquals(evictedValue1, "value1");
}
public void testRemove() throws InterruptedException, ExecutionException, TimeoutException {
final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
final List<String> removedValues = new ArrayList<>();
cache.addListener(event -> removedValues.add(event.getValue()), EventType.REMOVED);
cache.put("key", "value1");
final String removedValue = cache.remove("key");
Assert.assertEquals(removedValue, "value1");
Assert.assertEquals(removedValues, Arrays.asList("value1"));
Assert.assertEquals(cache.get("key"), null);
}
public void testClear() throws InterruptedException, ExecutionException, TimeoutException {
final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
final List<String> removedValues = new ArrayList<>();
cache.addListener(event -> removedValues.add(event.getValue()), EventType.REMOVED);
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.clear();
Assert.assertEquals(cache.get("key1"), null);
Assert.assertEquals(cache.get("key2"), null);
Assert.assertEquals(removedValues, Arrays.asList("value1", "value2"));
}
public void testForEachTouches() throws InterruptedException, ExecutionException, TimeoutException {
final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock();
final Duration timeToLive = Duration.ofSeconds(10);
final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive, clock);
final CompletableFuture<String> evictionEventFuture = new CompletableFuture<>();
cache.addListener(event -> {
evictionEventFuture.complete(event.getValue());
}, EventType.EVICTED);
// add value
cache.put("key", "value1");
// seek, so that it is almost evicted
clock.plus(timeToLive.minusMillis(1));
cache.updateTime();
// the for each should touch the entries
cache.forEach(s -> {
/* no-op */});
// seek again
clock.plus(timeToLive.minusMillis(1));
cache.triggerEvictionAndWait();
// if the touch didn't happen, then the value is now evicted
Assert.assertEquals(evictionEventFuture.isDone(), false);
// seek again, so that the entry will get evicted
clock.plus(timeToLive.minusMillis(1));
cache.triggerEvictionAndWait();
Assert.assertEquals(cache.get("key"), null);
}
/**
* Checks that
* {@link HotEntryCache#putIfAbsent(Object, java.util.function.Function)
* putIfAbsent} is atomic by calling
* {@link HotEntryCache#putIfAbsent(Object, java.util.function.Function)
* putIfAbsent} in two threads and asserting that the supplier was only called
* once.
*
* @throws Exception
*/
public void testPutIfAbsentIsAtomic() throws Exception {
final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
final ExecutorService pool = Executors.newCachedThreadPool();
try {
final CountDownLatch latch = new CountDownLatch(1);
final String key = "key";
final String valueA = "A";
final String valueB = "B";
pool.submit(() -> {
cache.putIfAbsent(key, k -> {
latch.countDown();
sleep(TimeUnit.MILLISECONDS, 20);
return valueA;
});
return null;
});
pool.submit(() -> {
waitFor(latch);
cache.putIfAbsent(key, k -> valueB);
return null;
});
pool.shutdown();
pool.awaitTermination(1, TimeUnit.MINUTES);
final String actual = cache.get(key);
Assert.assertEquals(actual, valueA);
} finally {
pool.shutdownNow();
}
}
public void testPutIfAbsentReturnsExistingValue() throws Exception {
final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
final String key = "key";
final String valueA = "A";
final String valueB = "B";
cache.put(key, valueA);
final String returnedByPutIfAbsent = cache.putIfAbsent(key, k -> valueB);
Assert.assertEquals(returnedByPutIfAbsent, valueA);
final String actualInCache = cache.get(key);
Assert.assertEquals(actualInCache, valueA);
}
public void testPutIfAbsentDoesNotAddNull() throws Exception {
final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
final String key = "key";
final String returnedByPutIfAbsent = cache.putIfAbsent(key, k -> null);
Assert.assertNull(returnedByPutIfAbsent, null);
final String actualInCache = cache.get(key);
Assert.assertEquals(actualInCache, null);
}
private void sleep(final TimeUnit timeUnit, final long timeout) {
try {
timeUnit.sleep(timeout);
} catch (final InterruptedException e) {
throw new IllegalStateException(e);
}
}
private void waitFor(final CountDownLatch latch) {
try {
latch.await(1, TimeUnit.MINUTES);
} catch (final InterruptedException e) {
throw new IllegalStateException(e);
}
}
public static void main(final String[] args) throws InterruptedException {
Configurator.setRootLevel(Level.TRACE);
final Duration timeToLive = Duration.ofSeconds(1);
final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive);
cache.addListener(event -> {
System.out.println(Instant.now() + " evicting: " + event);
}, EventType.EVICTED);
cache.put("key", "value that is touched");
for (int i = 0; i < 20; i++) {
System.out.println(Instant.now() + " putting value" + i);
cache.put("key" + i, "value" + i);
cache.put("key", "value that is touched" + i);
TimeUnit.MILLISECONDS.sleep(450);
}
for (int i = 20; i < 23; i++) {
System.out.println(Instant.now() + " putting value" + i);
cache.put("key" + i, "value" + i);
TimeUnit.MILLISECONDS.sleep(Duration.ofSeconds(5).plusMillis(10).toMillis());
}
TimeUnit.MILLISECONDS.sleep(Duration.ofSeconds(5).plusMillis(10).toMillis());
for (int i = 23; i < 27; i++) {
System.out.println(Instant.now() + " putting value" + i);
cache.put("key" + i, "value" + i);
TimeUnit.MILLISECONDS.sleep(Duration.ofSeconds(5).plusMillis(10).toMillis());
}
TimeUnit.SECONDS.sleep(300);
}
}
//package org.lucares.utils.cache;
//
//import java.time.Duration;
//import java.time.Instant;
//import java.util.ArrayList;
//import java.util.Arrays;
//import java.util.List;
//import java.util.concurrent.CompletableFuture;
//import java.util.concurrent.CountDownLatch;
//import java.util.concurrent.ExecutionException;
//import java.util.concurrent.ExecutorService;
//import java.util.concurrent.Executors;
//import java.util.concurrent.TimeUnit;
//import java.util.concurrent.TimeoutException;
//
//import org.apache.logging.log4j.Level;
//import org.apache.logging.log4j.core.config.Configurator;
//import org.lucares.utils.cache.HotEntryCache.EventType;
//import org.testng.Assert;
//import org.testng.annotations.Test;
//
//@Test
//public class HotEntryCacheTest {
// public void testPutAndGet() throws InterruptedException, ExecutionException, TimeoutException {
// final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
//
// final String replacedNull = cache.put("key", "value1");
// Assert.assertEquals(replacedNull, null);
//
// final String cachedValue1 = cache.get("key");
// Assert.assertEquals(cachedValue1, "value1");
//
// final String replacedValue1 = cache.put("key", "value2");
// Assert.assertEquals(replacedValue1, "value1");
//
// final String cachedValue2 = cache.get("key");
// Assert.assertEquals(cachedValue2, "value2");
// }
//
// public void testPutTouches() throws InterruptedException, ExecutionException, TimeoutException {
// final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock();
// final Duration timeToLive = Duration.ofSeconds(10);
// final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive, clock);
//
// cache.put("key", "value1");
//
// clock.plusSeconds(2);
// cache.updateTime();
//
// cache.put("key", "value2");
//
// clock.plus(timeToLive.minusSeconds(1));
// cache.triggerEvictionAndWait();
// // at this point the entry would have been evicted it it was not touched by the
// // second put.
//
// final String cachedValue2 = cache.get("key");
// Assert.assertEquals(cachedValue2, "value2");
//
// clock.plus(timeToLive.plusSeconds(1));
// // time elapsed since the last put: timeToLive +1s
// cache.triggerEvictionAndWait();
//
// final String cachedValue1_evicted = cache.get("key");
// Assert.assertEquals(cachedValue1_evicted, null);
// }
//
// public void testGetTouches() throws Exception {
// final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock();
// final Duration timeToLive = Duration.ofSeconds(10);
// final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive, clock);
//
// cache.put("key", "value1");
//
// // skip forward in time, but do not yet trigger eviction
// clock.plus(timeToLive.plusMillis(1));
// cache.updateTime();
//
// cache.get("key"); // will touch the entry
//
// cache.triggerEvictionAndWait(); // if get didn't touch, then this will evict the entry
//
// final String cachedValue1 = cache.get("key");
// Assert.assertEquals(cachedValue1, "value1");
// }
//
// public void testEvictionByBackgroundThread() throws InterruptedException, ExecutionException, TimeoutException {
// final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock();
// final Duration timeToLive = Duration.ofSeconds(10);
// final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive, clock);
//
// final CompletableFuture<String> evictionEventFuture = new CompletableFuture<>();
// cache.addListener(event -> {
// evictionEventFuture.complete(event.getValue());
// }, EventType.EVICTED);
//
// cache.put("key", "value1");
//
// clock.plus(timeToLive.minusSeconds(1));
// cache.updateTime();
//
// cache.put("key2", "value2");
// clock.plus(Duration.ofSeconds(1).plusMillis(1));
// cache.triggerEvictionAndWait();
//
// final String evictedValue1 = evictionEventFuture.get(5, TimeUnit.MINUTES); // enough time for debugging
// Assert.assertEquals(evictedValue1, "value1");
// }
//
// public void testRemove() throws InterruptedException, ExecutionException, TimeoutException {
// final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
//
// final List<String> removedValues = new ArrayList<>();
// cache.addListener(event -> removedValues.add(event.getValue()), EventType.REMOVED);
//
// cache.put("key", "value1");
//
// final String removedValue = cache.remove("key");
// Assert.assertEquals(removedValue, "value1");
//
// Assert.assertEquals(removedValues, Arrays.asList("value1"));
//
// Assert.assertEquals(cache.get("key"), null);
// }
//
// public void testClear() throws InterruptedException, ExecutionException, TimeoutException {
// final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
//
// final List<String> removedValues = new ArrayList<>();
// cache.addListener(event -> removedValues.add(event.getValue()), EventType.REMOVED);
//
// cache.put("key1", "value1");
// cache.put("key2", "value2");
//
// cache.clear();
//
// Assert.assertEquals(cache.get("key1"), null);
// Assert.assertEquals(cache.get("key2"), null);
//
// Assert.assertEquals(removedValues, Arrays.asList("value1", "value2"));
// }
//
// public void testForEachTouches() throws InterruptedException, ExecutionException, TimeoutException {
// final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock();
// final Duration timeToLive = Duration.ofSeconds(10);
// final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive, clock);
//
// final CompletableFuture<String> evictionEventFuture = new CompletableFuture<>();
// cache.addListener(event -> {
// evictionEventFuture.complete(event.getValue());
// }, EventType.EVICTED);
//
// // add value
// cache.put("key", "value1");
//
// // seek, so that it is almost evicted
// clock.plus(timeToLive.minusMillis(1));
// cache.updateTime();
//
// // the for each should touch the entries
// cache.forEach(s -> {
// /* no-op */});
//
// // seek again
// clock.plus(timeToLive.minusMillis(1));
// cache.triggerEvictionAndWait();
//
// // if the touch didn't happen, then the value is now evicted
// Assert.assertEquals(evictionEventFuture.isDone(), false);
//
// // seek again, so that the entry will get evicted
// clock.plus(timeToLive.minusMillis(1));
// cache.triggerEvictionAndWait();
//
// Assert.assertEquals(cache.get("key"), null);
// }
//
// /**
// * Checks that
// * {@link HotEntryCache#putIfAbsent(Object, java.util.function.Function)
// * putIfAbsent} is atomic by calling
// * {@link HotEntryCache#putIfAbsent(Object, java.util.function.Function)
// * putIfAbsent} in two threads and asserting that the supplier was only called
// * once.
// *
// * @throws Exception
// */
// public void testPutIfAbsentIsAtomic() throws Exception {
// final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
//
// final ExecutorService pool = Executors.newCachedThreadPool();
// try {
// final CountDownLatch latch = new CountDownLatch(1);
//
// final String key = "key";
// final String valueA = "A";
// final String valueB = "B";
//
// pool.submit(() -> {
// cache.putIfAbsent(key, k -> {
// latch.countDown();
// sleep(TimeUnit.MILLISECONDS, 20);
// return valueA;
// });
// return null;
// });
// pool.submit(() -> {
// waitFor(latch);
// cache.putIfAbsent(key, k -> valueB);
// return null;
// });
//
// pool.shutdown();
// pool.awaitTermination(1, TimeUnit.MINUTES);
//
// final String actual = cache.get(key);
// Assert.assertEquals(actual, valueA);
// } finally {
// pool.shutdownNow();
// }
// }
//
// public void testPutIfAbsentReturnsExistingValue() throws Exception {
// final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
//
// final String key = "key";
// final String valueA = "A";
// final String valueB = "B";
//
// cache.put(key, valueA);
//
// final String returnedByPutIfAbsent = cache.putIfAbsent(key, k -> valueB);
// Assert.assertEquals(returnedByPutIfAbsent, valueA);
//
// final String actualInCache = cache.get(key);
// Assert.assertEquals(actualInCache, valueA);
// }
//
// public void testPutIfAbsentDoesNotAddNull() throws Exception {
// final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofSeconds(10));
//
// final String key = "key";
// final String returnedByPutIfAbsent = cache.putIfAbsent(key, k -> null);
// Assert.assertNull(returnedByPutIfAbsent, null);
//
// final String actualInCache = cache.get(key);
// Assert.assertEquals(actualInCache, null);
// }
//
// private void sleep(final TimeUnit timeUnit, final long timeout) {
// try {
// timeUnit.sleep(timeout);
// } catch (final InterruptedException e) {
// throw new IllegalStateException(e);
// }
// }
//
// private void waitFor(final CountDownLatch latch) {
// try {
// latch.await(1, TimeUnit.MINUTES);
// } catch (final InterruptedException e) {
// throw new IllegalStateException(e);
// }
// }
//
// public static void main(final String[] args) throws InterruptedException {
//
// Configurator.setRootLevel(Level.TRACE);
//
// final Duration timeToLive = Duration.ofSeconds(1);
// final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive);
//
// cache.addListener(event -> {
// System.out.println(Instant.now() + " evicting: " + event);
// }, EventType.EVICTED);
// cache.put("key", "value that is touched");
// for (int i = 0; i < 20; i++) {
//
// System.out.println(Instant.now() + " putting value" + i);
// cache.put("key" + i, "value" + i);
// cache.put("key", "value that is touched" + i);
// TimeUnit.MILLISECONDS.sleep(450);
// }
//
// for (int i = 20; i < 23; i++) {
// System.out.println(Instant.now() + " putting value" + i);
// cache.put("key" + i, "value" + i);
// TimeUnit.MILLISECONDS.sleep(Duration.ofSeconds(5).plusMillis(10).toMillis());
// }
//
// TimeUnit.MILLISECONDS.sleep(Duration.ofSeconds(5).plusMillis(10).toMillis());
//
// for (int i = 23; i < 27; i++) {
// System.out.println(Instant.now() + " putting value" + i);
// cache.put("key" + i, "value" + i);
// TimeUnit.MILLISECONDS.sleep(Duration.ofSeconds(5).plusMillis(10).toMillis());
// }
//
// TimeUnit.SECONDS.sleep(300);
// }
//}