From 98892522057e61495768ed5bc1c0ff56986e0c0d Mon Sep 17 00:00:00 2001 From: Andreas Huber Date: Sat, 24 Nov 2018 08:32:05 +0100 Subject: [PATCH] use only one thread for evictions Instead of spawning a new thread for every cache, we use a single thread that will evict entries from all caches. The thread keeps a weak reference to the caches, so that they can be garbage collected. --- .../lucares/utils/cache/HotEntryCache.java | 92 ++++++++++++++++--- .../utils/cache/HotEntryCacheTest.java | 8 +- 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/pdb-utils/src/main/java/org/lucares/utils/cache/HotEntryCache.java b/pdb-utils/src/main/java/org/lucares/utils/cache/HotEntryCache.java index 28c1274..86f49ef 100644 --- a/pdb-utils/src/main/java/org/lucares/utils/cache/HotEntryCache.java +++ b/pdb-utils/src/main/java/org/lucares/utils/cache/HotEntryCache.java @@ -5,17 +5,15 @@ import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.EnumSet; +import java.util.Set; +import java.util.WeakHashMap; 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. @@ -103,10 +101,64 @@ public class HotEntryCache { } } - private final ConcurrentHashMap> cache = new ConcurrentHashMap<>(); + private static final class EvictionThread extends Thread { - private final ScheduledExecutorService evicter = Executors.newScheduledThreadPool(1, - new ThreadFactoryBuilder().setNameFormat("eviction-%d").setDaemon(true).build()); + private static final Duration MAX_SLEEP_PERIOD = Duration.ofDays(1); + private final WeakHashMap, Void> weakCaches = new WeakHashMap<>(); + + public EvictionThread() { + setDaemon(true); + setName("HotEntryCache-eviction"); + } + + public void addCache(final HotEntryCache cache) { + weakCaches.put(cache, null); + } + + @Override + public void run() { + Duration minTimeToNextEviction = MAX_SLEEP_PERIOD; + + while (true) { + try { + final long timeToSleepMS = Math.max( + minTimeToNextEviction.compareTo(MAX_SLEEP_PERIOD) < 0 ? minTimeToNextEviction.toMillis() + : MAX_SLEEP_PERIOD.toMillis(), + 1); + + TimeUnit.MILLISECONDS.sleep(timeToSleepMS); + } catch (final InterruptedException e) { + // interrupted: evict stale elements from all caches and compute the delay until + // the next check + } + + minTimeToNextEviction = Duration.ofMillis(Long.MAX_VALUE); + final Set> caches = weakCaches.keySet(); + for (final HotEntryCache cache : caches) { + + final Duration timeToNextEviction = cache.evict(); + + if (!timeToNextEviction.isNegative()) { + minTimeToNextEviction = minTimeToNextEviction.compareTo(timeToNextEviction) < 0 + ? minTimeToNextEviction + : timeToNextEviction; + } + } + } + } + + public void nextEvictionChanged() { + this.interrupt(); + } + } + + private static final EvictionThread EVICTER = new EvictionThread(); + + static { + EVICTER.start(); + } + + private final ConcurrentHashMap> cache = new ConcurrentHashMap<>(); private final CopyOnWriteArrayList> listeners = new CopyOnWriteArrayList<>(); @@ -116,15 +168,14 @@ public class HotEntryCache { private Instant nextEviction = Instant.MAX; - HotEntryCache(final Duration timeToLive, final Clock clock, final long delayForEvictionThread, - final TimeUnit timeUnit) { + HotEntryCache(final Duration timeToLive, final Clock clock) { this.timeToLive = timeToLive; this.clock = clock; - evicter.scheduleWithFixedDelay(this::evict, delayForEvictionThread, delayForEvictionThread, timeUnit); + EVICTER.addCache(this); } public HotEntryCache(final Duration timeToLive) { - this(timeToLive, Clock.systemDefaultZone(), 5, TimeUnit.SECONDS); + this(timeToLive, Clock.systemDefaultZone()); } public void addListener(final EventListener listener, final EventType... eventTypes) { @@ -206,7 +257,7 @@ public class HotEntryCache { }); } - private void evict() { + private Duration evict() { final Instant now = Instant.now(clock); if (nextEviction.isBefore(now)) { @@ -220,16 +271,26 @@ public class HotEntryCache { }); } } + return Duration.between(now, nextEviction); } private void touch(final Entry entry) { if (entry != null) { final Instant now = Instant.now(clock); entry.touch(now); - nextEviction = now.plus(timeToLive); + updateNextEviction(now.plus(timeToLive)); } } + private void updateNextEviction(final Instant nextEviction) { + + if (this.nextEviction.isAfter(nextEviction)) { + EVICTER.nextEvictionChanged(); + } + + this.nextEviction = nextEviction; + } + private boolean isExpired(final Entry entry, final Instant now) { return entry.getLastAccessed().plus(timeToLive).isBefore(now); } @@ -242,4 +303,9 @@ public class HotEntryCache { } } } + + // visible for test + void triggerEviction() { + EVICTER.nextEvictionChanged(); + } } diff --git a/pdb-utils/src/test/java/org/lucares/utils/cache/HotEntryCacheTest.java b/pdb-utils/src/test/java/org/lucares/utils/cache/HotEntryCacheTest.java index 5665ff7..55afaa1 100644 --- a/pdb-utils/src/test/java/org/lucares/utils/cache/HotEntryCacheTest.java +++ b/pdb-utils/src/test/java/org/lucares/utils/cache/HotEntryCacheTest.java @@ -37,11 +37,12 @@ public class HotEntryCacheTest { public void testEvictOnGet() throws InterruptedException, ExecutionException, TimeoutException { final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock(); final Duration timeToLive = Duration.ofSeconds(10); - final HotEntryCache cache = new HotEntryCache<>(timeToLive, clock, 1, TimeUnit.MILLISECONDS); + final HotEntryCache cache = new HotEntryCache<>(timeToLive, clock); cache.put("key", "value1"); clock.plus(timeToLive.plusMillis(1)); + cache.triggerEviction(); final String cachedValue1_evicted = cache.get("key"); Assert.assertEquals(cachedValue1_evicted, null); @@ -50,7 +51,7 @@ public class HotEntryCacheTest { public void testEvictionByBackgroundThread() throws InterruptedException, ExecutionException, TimeoutException { final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock(); final Duration timeToLive = Duration.ofSeconds(10); - final HotEntryCache cache = new HotEntryCache<>(timeToLive, clock, 1, TimeUnit.MILLISECONDS); + final HotEntryCache cache = new HotEntryCache<>(timeToLive, clock); final CompletableFuture evictionEventFuture = new CompletableFuture<>(); cache.addListener(event -> { @@ -60,6 +61,7 @@ public class HotEntryCacheTest { cache.put("key", "value1"); clock.plus(timeToLive.plusMillis(1)); + cache.triggerEviction(); final String evictedValue1 = evictionEventFuture.get(5, TimeUnit.MINUTES); // enough time for debugging Assert.assertEquals(evictedValue1, "value1"); @@ -101,7 +103,7 @@ public class HotEntryCacheTest { public void testForEachTouches() throws InterruptedException, ExecutionException, TimeoutException { final ModifiableFixedTimeClock clock = new ModifiableFixedTimeClock(); final Duration timeToLive = Duration.ofSeconds(10); - final HotEntryCache cache = new HotEntryCache<>(timeToLive, clock, 1, TimeUnit.HOURS); + final HotEntryCache cache = new HotEntryCache<>(timeToLive, clock); final CompletableFuture evictionEventFuture = new CompletableFuture<>(); cache.addListener(event -> {