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,231 @@
package org.lucares.utils.cache;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
/**
* A cache that only keeps 'hot' entries, that is entries that have been
* accessed recently. Entries that have not been accessed recently are removed.
* <p>
* Caching frameworks like EhCache only evict entries when a new entry is added.
* That might not be desired, e.g. when the cached objects block resources.
* <p>
* This cache is a simple wrapper for a ConcurrentHashMap that evicts entries
* after timeToLive+5s.
*/
public class HotEntryCache<K, V> {
public enum EventType {
EVICTED, REMOVED
}
public interface EventListener<K, V> {
public void onEvent(Event<K, V> event);
}
public static class Event<K, V> {
private final EventType eventType;
private final K key;
private final V value;
public Event(final EventType eventType, final K key, final V value) {
super();
this.eventType = eventType;
this.key = key;
this.value = value;
}
public EventType getEventType() {
return eventType;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
private final static class EventSubscribers<K, V> {
private final EnumSet<EventType> subscribedEvents;
private final EventListener<K, V> listener;
public EventSubscribers(final EnumSet<EventType> subscribedEvents, final EventListener<K, V> listener) {
super();
this.subscribedEvents = subscribedEvents;
this.listener = listener;
}
public EnumSet<EventType> getSubscribedEvents() {
return subscribedEvents;
}
public EventListener<K, V> getListener() {
return listener;
}
}
private final static class Entry<V> {
private Instant lastAccessed;
private final V value;
public Entry(final V value, final Clock clock) {
this.value = value;
lastAccessed = Instant.now(clock);
}
public Instant getLastAccessed() {
return lastAccessed;
}
public V getValue() {
return value;
}
public void touch(final Clock clock) {
lastAccessed = Instant.now(clock);
}
}
private final ConcurrentHashMap<K, Entry<V>> cache = new ConcurrentHashMap<>();
private final ScheduledExecutorService evicter = Executors.newScheduledThreadPool(1,
new ThreadFactoryBuilder().setNameFormat("eviction-%d").setDaemon(true).build());
private final CopyOnWriteArrayList<EventSubscribers<K, V>> listeners = new CopyOnWriteArrayList<>();
private final Duration timeToLive;
private Clock clock;
HotEntryCache(final Duration timeToLive, final Clock clock, final long delayForEvictionThread,
final TimeUnit timeUnit) {
this.timeToLive = timeToLive;
this.clock = clock;
evicter.scheduleWithFixedDelay(this::evict, delayForEvictionThread, delayForEvictionThread, timeUnit);
}
public HotEntryCache(final Duration timeToLive) {
this(timeToLive, Clock.systemDefaultZone(), 5, TimeUnit.SECONDS);
}
public void addListener(final EventListener<K, V> listener, final EventType... eventTypes) {
listeners.add(new EventSubscribers<>(EnumSet.copyOf(Arrays.asList(eventTypes)), listener));
}
public V get(final K key) {
final Entry<V> entry = cache.computeIfPresent(key, (k, e) -> {
if (isExpired(e)) {
handleEvent(EventType.EVICTED, k, e.getValue());
return null;
}
e.touch(clock);
return e;
});
return entry != null ? entry.getValue() : null;
}
public V put(final K key, final V value) {
final AtomicReference<Entry<V>> oldValue = new AtomicReference<>();
cache.compute(key, (k, v) -> {
oldValue.set(v);
return new Entry<>(value, clock);
});
return oldValue.get() != null ? oldValue.get().getValue() : null;
}
/**
* Puts the value supplied by the mappingFunction, if the key does not already
* exist in the map. The operation is done atomically, that is the function is
* executed at most once. This method is blocking while other threads are
* computing the mapping function. Therefore the computation should be short and
* simple.
*
* @param key key of the value
* @param mappingFunction a function that returns the value that should be
* inserted
* @return the newly inserted or existing value, or null if
* {@code mappingFunction} returned {@code null}
*/
public V putIfAbsent(final K key, final Function<K, V> mappingFunction) {
final Entry<V> entry = cache.computeIfAbsent(key, (k) -> {
final V value = mappingFunction.apply(k);
return new Entry<>(value, clock);
});
if (entry != null) {
entry.touch(clock);
}
return entry != null ? entry.getValue() : null;
}
public V remove(final K key) {
final AtomicReference<Entry<V>> oldValue = new AtomicReference<>();
cache.computeIfPresent(key, (k, e) -> {
oldValue.set(e);
handleEvent(EventType.REMOVED, k, e.getValue());
return null;
});
return oldValue.get() != null ? oldValue.get().getValue() : null;
}
public void clear() {
for (final K key : cache.keySet()) {
remove(key);
}
}
public void forEach(final Consumer<V> consumer) {
cache.forEachValue(Long.MAX_VALUE, entry -> {
entry.touch(clock);
consumer.accept(entry.getValue());
});
}
private void evict() {
for (final K key : cache.keySet()) {
cache.computeIfPresent(key, (k, e) -> {
if (isExpired(e)) {
handleEvent(EventType.EVICTED, k, e.getValue());
return null;
}
return e;
});
}
}
private boolean isExpired(final Entry<V> entry) {
return entry.getLastAccessed().plus(timeToLive).isBefore(Instant.now(clock));
}
private void handleEvent(final EventType eventType, final K key, final V value) {
for (final EventSubscribers<K, V> eventSubscribers : listeners) {
if (eventSubscribers.getSubscribedEvents().contains(eventType)) {
eventSubscribers.getListener().onEvent(new Event<>(eventType, key, value));
}
}
}
}