add cache for docId to Doc mapping

A Doc does not change once it is created, so it is easy to cache.
Speedup was from 1ms per Doc to 3ms for 444 Docs (0.00675ms/Doc).
This commit is contained in:
2018-11-22 19:51:07 +01:00
parent 6c546bd5b3
commit f78f69328b
6 changed files with 158 additions and 17 deletions

View File

@@ -0,0 +1,220 @@
package org.lucares.utils.cache;
import java.time.Duration;
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.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 testEvictOnGet() throws InterruptedException, ExecutionException, TimeoutException {
final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock();
final Duration timeToLive = Duration.ofSeconds(10);
final HotEntryCache<String, String> cache = new HotEntryCache<>(timeToLive, clock, 1, TimeUnit.MILLISECONDS);
cache.put("key", "value1");
clock.plus(timeToLive.plusMillis(1));
final String cachedValue1_evicted = cache.get("key");
Assert.assertEquals(cachedValue1_evicted, null);
}
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, 1, TimeUnit.MILLISECONDS);
final CompletableFuture<String> evictionEventFuture = new CompletableFuture<>();
cache.addListener(event -> {
evictionEventFuture.complete(event.getValue());
}, EventType.EVICTED);
cache.put("key", "value1");
clock.plus(timeToLive.plusMillis(1));
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, 1, TimeUnit.HOURS);
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));
// the for each should touch the entries
cache.forEach(s -> {
/* no-op */});
// seek again
clock.plus(timeToLive.minusMillis(1));
// 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));
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);
}
}
}

View File

@@ -0,0 +1,98 @@
package org.lucares.utils.cache;
import java.io.Serializable;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalUnit;
/**
* A {@link Clock} with a fixed, but modifiable, time. This {@link Clock} is
* useful in tests, so that you can explicitly set the time.
*/
public class ModifiableFixedTimeClock extends Clock implements Serializable {
private static final long serialVersionUID = 1955332545617873736L;
private Instant instant;
private final ZoneId zone;
public ModifiableFixedTimeClock() {
this(Instant.now());
}
public ModifiableFixedTimeClock(final Instant fixedInstant) {
this(fixedInstant, ZoneId.systemDefault());
}
public ModifiableFixedTimeClock(final Instant fixedInstant, final ZoneId zone) {
this.instant = fixedInstant;
this.zone = zone;
}
public void setTime(final Instant instant) {
this.instant = instant;
}
public void plus(final TemporalAmount amountToAdd) {
instant = instant.plus(amountToAdd);
}
public void plus(final long amountToAdd, final TemporalUnit unit) {
instant = instant.plus(amountToAdd, unit);
}
public void plusMillis(final long millisToAdd) {
instant = instant.plusMillis(millisToAdd);
}
public void plusNanos(final long nanosToAdd) {
instant = instant.plusNanos(nanosToAdd);
}
public void plusSeconds(final long secondsToAdd) {
instant = instant.plusSeconds(secondsToAdd);
}
@Override
public ZoneId getZone() {
return zone;
}
@Override
public Clock withZone(final ZoneId zone) {
if (zone.equals(this.zone)) {
return this;
}
return new ModifiableFixedTimeClock(instant, zone);
}
@Override
public long millis() {
return instant.toEpochMilli();
}
@Override
public Instant instant() {
return instant;
}
@Override
public boolean equals(final Object obj) {
if (obj instanceof ModifiableFixedTimeClock) {
final ModifiableFixedTimeClock other = (ModifiableFixedTimeClock) obj;
return instant.equals(other.instant) && zone.equals(other.zone);
}
return false;
}
@Override
public int hashCode() {
return instant.hashCode() ^ zone.hashCode();
}
@Override
public String toString() {
return "FixedClock[" + instant + "," + zone + "]";
}
}