use long instead of Instant for time

Working with longs is faster and requires less
cache. The space in L123 caches is precious.
This commit is contained in:
2019-08-19 18:58:24 +02:00
parent feda901f6d
commit 00c20dae6b
2 changed files with 70 additions and 73 deletions

View File

@@ -2,7 +2,6 @@ package org.lucares.utils.cache;
import java.time.Clock; import java.time.Clock;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.util.ConcurrentModificationException; import java.util.ConcurrentModificationException;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@@ -31,8 +30,16 @@ import org.slf4j.LoggerFactory;
* Caching frameworks like EhCache only evict entries when a new entry is added. * 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. * That might not be desired, e.g. when the cached objects block resources.
* <p> * <p>
* This cache is a simple wrapper for a ConcurrentHashMap that evicts entries * This cache is a wrapper for a ConcurrentHashMap that evicts entries after the
* after timeToLive+5s. * specified timeToLive. The cache spawns two threads.
* <ol>
* <li>One for updating time. It is far too expensive to call
* {@link System#currentTimeMillis()} or something similar whenever a timestamp
* is needed. Therefore we have a thread that updates a member periodically.
* <li>One for evicting elements. So that elements can be evicted based on their
* time to live. Other cache implementations do this only when elements are
* added to the cache.
* </ol>
*/ */
public class HotEntryCache<K, V> { public class HotEntryCache<K, V> {
@@ -67,11 +74,11 @@ public class HotEntryCache<K, V> {
} }
private final static class Entry<V> { private final static class Entry<V> {
private Instant lastAccessed; private long lastAccessed;
private V value; private V value;
public Entry(final V value, final Instant creationTime) { public Entry(final V value, final long creationTime) {
this.value = value; this.value = value;
lastAccessed = creationTime; lastAccessed = creationTime;
} }
@@ -84,11 +91,11 @@ public class HotEntryCache<K, V> {
this.value = value; this.value = value;
} }
public Instant getLastAccessed() { public long getLastAccessed() {
return lastAccessed; return lastAccessed;
} }
public void touch(final Instant instant) { public void touch(final long instant) {
lastAccessed = instant; lastAccessed = instant;
} }
} }
@@ -110,8 +117,8 @@ public class HotEntryCache<K, V> {
} }
} }
public void setUpdateInterval(final Duration updateInterval) { public void setUpdateInterval(final long updateInterval) {
this.updateInterval = Math.max(updateInterval.toMillis(), 1); this.updateInterval = Math.max(updateInterval, 1);
interrupt(); interrupt();
} }
@@ -147,8 +154,8 @@ public class HotEntryCache<K, V> {
private final AtomicReference<CompletableFuture<Void>> future = new AtomicReference<>(null); private final AtomicReference<CompletableFuture<Void>> future = new AtomicReference<>(null);
private final Object lock = new Object(); private final Object lock = new Object();
private Duration minSleepPeriod = Duration.ofSeconds(5); private long minSleepPeriodInMs = Duration.ofSeconds(5).toMillis();
private Duration maxSleepPeriod = Duration.ofDays(1); private long maxSleepPeriodInMs = Duration.ofDays(1).toMillis();
public EvictionThread() { public EvictionThread() {
setDaemon(true); setDaemon(true);
@@ -161,17 +168,17 @@ public class HotEntryCache<K, V> {
} }
} }
private Duration getMinSleepPeriod() { private long getMinSleepPeriod() {
return minSleepPeriod; return minSleepPeriodInMs;
} }
private Duration getMaxSleepPeriod() { private long getMaxSleepPeriod() {
return maxSleepPeriod; return maxSleepPeriodInMs;
} }
@Override @Override
public void run() { public void run() {
Duration timeToNextEviction = maxSleepPeriod; long timeToNextEviction = maxSleepPeriodInMs;
while (true) { while (true) {
sleepToNextEviction(timeToNextEviction); sleepToNextEviction(timeToNextEviction);
@@ -179,7 +186,7 @@ public class HotEntryCache<K, V> {
final CompletableFuture<Void> future = this.future.getAcquire(); final CompletableFuture<Void> future = this.future.getAcquire();
try { try {
final Instant minNextEvictionTime = evictStaleEntries(); final long minNextEvictionTime = evictStaleEntries();
timeToNextEviction = normalizeDurationToNextEviction(minNextEvictionTime); timeToNextEviction = normalizeDurationToNextEviction(minNextEvictionTime);
@@ -194,34 +201,34 @@ public class HotEntryCache<K, V> {
} }
} }
private Duration normalizeDurationToNextEviction(final Instant minNextEvictionTime) { private long normalizeDurationToNextEviction(final long minNextEvictionTime) {
Duration timeToNextEviction; long timeToNextEviction;
if (!minNextEvictionTime.equals(Instant.MAX)) { if (minNextEvictionTime != Long.MAX_VALUE) {
timeToNextEviction = minSleepPeriod; timeToNextEviction = minSleepPeriodInMs;
} else { } else {
final Instant now = Instant.now(); final long now = System.currentTimeMillis();
timeToNextEviction = Duration.between(now, minNextEvictionTime); timeToNextEviction = minNextEvictionTime - now;
} }
return timeToNextEviction; return timeToNextEviction;
} }
private Instant evictStaleEntries() { private long evictStaleEntries() {
Instant minNextEvictionTime = Instant.MAX; long minNextEvictionTime = Long.MAX_VALUE;
final Set<HotEntryCache<?, ?>> caches = new HashSet<>(); final Set<HotEntryCache<?, ?>> caches = new HashSet<>();
synchronized (lock) { synchronized (lock) {
caches.addAll(weakCaches.keySet()); caches.addAll(weakCaches.keySet());
} }
for (final HotEntryCache<?, ?> cache : caches) { for (final HotEntryCache<?, ?> cache : caches) {
final Instant nextEvictionTime = cache.evict(); final long nextEvictionTime = cache.evict();
minNextEvictionTime = min(minNextEvictionTime, nextEvictionTime); minNextEvictionTime = Math.min(minNextEvictionTime, nextEvictionTime);
} }
return minNextEvictionTime; return minNextEvictionTime;
} }
private void sleepToNextEviction(final Duration timeToNextEviction) { private void sleepToNextEviction(final long timeToNextEviction) {
try { try {
final Duration timeToSleep = minDuration(timeToNextEviction, maxSleepPeriod); final long timeToSleep = Math.min(timeToNextEviction, maxSleepPeriodInMs);
final long timeToSleepMS = Math.max(timeToSleep.toMillis(), minSleepPeriod.toMillis()); final long timeToSleepMS = Math.max(timeToSleep, minSleepPeriodInMs);
LOGGER.trace("sleeping {}ms", timeToSleepMS); LOGGER.trace("sleeping {}ms", timeToSleepMS);
TimeUnit.MILLISECONDS.sleep(timeToSleepMS); TimeUnit.MILLISECONDS.sleep(timeToSleepMS);
} catch (final InterruptedException e) { } catch (final InterruptedException e) {
@@ -247,12 +254,12 @@ public class HotEntryCache<K, V> {
return result; return result;
} }
public void setMinSleepPeriod(final Duration minSleepPeriod) { public void setMinSleepPeriod(final long minSleepPeriodInMs) {
this.minSleepPeriod = minSleepPeriod; this.minSleepPeriodInMs = minSleepPeriodInMs;
} }
public void setMaxSleepPeriod(final Duration maxSleepPeriod) { public void setMaxSleepPeriod(final long maxSleepPeriodInMs) {
this.maxSleepPeriod = maxSleepPeriod; this.maxSleepPeriodInMs = maxSleepPeriodInMs;
} }
} }
@@ -265,7 +272,7 @@ public class HotEntryCache<K, V> {
TIME_UPDATER.start(); TIME_UPDATER.start();
} }
private static Instant now; private static long now;
/** /**
* Mapping of the key to the value. * Mapping of the key to the value.
@@ -286,7 +293,7 @@ public class HotEntryCache<K, V> {
this.timeToLive = timeToLive; this.timeToLive = timeToLive;
this.clock = clock; this.clock = clock;
this.name = name; this.name = name;
now = Instant.now(clock); now = clock.millis();
EVICTER.addCache(this); EVICTER.addCache(this);
TIME_UPDATER.addCache(this); TIME_UPDATER.addCache(this);
@@ -316,20 +323,20 @@ public class HotEntryCache<K, V> {
listeners.add(listener); listeners.add(listener);
} }
static void setMinSleepPeriod(final Duration minSleepPeriod) { static void setMinSleepPeriod(final long minSleepPeriodInMs) {
EVICTER.setMinSleepPeriod(minSleepPeriod); EVICTER.setMinSleepPeriod(minSleepPeriodInMs);
TIME_UPDATER.setUpdateInterval(minSleepPeriod.dividedBy(2)); TIME_UPDATER.setUpdateInterval(minSleepPeriodInMs / 2);
} }
static void setMaxSleepPeriod(final Duration maxSleepPeriod) { static void setMaxSleepPeriod(final long maxSleepPeriodInMs) {
EVICTER.setMaxSleepPeriod(maxSleepPeriod); EVICTER.setMaxSleepPeriod(maxSleepPeriodInMs);
} }
static Duration getMinSleepPeriod() { static long getMinSleepPeriod() {
return EVICTER.getMinSleepPeriod(); return EVICTER.getMinSleepPeriod();
} }
static Duration getMaxSleepPeriod() { static long getMaxSleepPeriod() {
return EVICTER.getMaxSleepPeriod(); return EVICTER.getMaxSleepPeriod();
} }
@@ -354,7 +361,7 @@ public class HotEntryCache<K, V> {
oldEntry.setValue(value); oldEntry.setValue(value);
entry = oldEntry; entry = oldEntry;
} else { } else {
final Instant creationTime = now(); final long creationTime = now();
entry = new Entry<>(value, creationTime); entry = new Entry<>(value, creationTime);
} }
touch(k, entry); touch(k, entry);
@@ -387,7 +394,7 @@ public class HotEntryCache<K, V> {
final boolean wasEmptyBefore = cache.isEmpty(); final boolean wasEmptyBefore = cache.isEmpty();
final Entry<V> entry = cache.computeIfAbsent(key, (k) -> { final Entry<V> entry = cache.computeIfAbsent(key, (k) -> {
final V value = mappingFunction.apply(k); final V value = mappingFunction.apply(k);
final Instant creationTime = now(); final long creationTime = now();
final Entry<V> e = new Entry<>(value, creationTime); final Entry<V> e = new Entry<>(value, creationTime);
touch(key, e); touch(key, e);
return e; return e;
@@ -426,20 +433,18 @@ public class HotEntryCache<K, V> {
}); });
} }
private Instant evict() { private long evict() {
final Instant now = now(); final long now = now();
final Instant oldestValuesToKeep = now.minus(timeToLive); final long oldestValuesToKeep = now - timeToLive.toMillis();
Instant lastAccessTime = Instant.MAX; long lastAccessTime = Long.MAX_VALUE;
LOGGER.trace("{}: cache size before eviction {}", name, cache.size()); LOGGER.trace("{}: cache size before eviction {}", name, cache.size());
// for (final java.util.Map.Entry<Instant, Set<K>> mapEntry :
// lastAccessMap.entrySet()) {
for (final java.util.Map.Entry<K, Entry<V>> mapEntry : cache.entrySet()) { for (final java.util.Map.Entry<K, Entry<V>> mapEntry : cache.entrySet()) {
final Entry<V> entry = mapEntry.getValue(); final Entry<V> entry = mapEntry.getValue();
final Instant lastAccessed = entry.getLastAccessed(); final long lastAccessed = entry.getLastAccessed();
lastAccessTime = min(lastAccessTime, lastAccessed); lastAccessTime = Math.min(lastAccessTime, lastAccessed);
if (lastAccessed.isAfter(oldestValuesToKeep)) { if (lastAccessed > oldestValuesToKeep) {
continue; continue;
} }
@@ -456,38 +461,30 @@ public class HotEntryCache<K, V> {
} }
LOGGER.trace("{}: cache size after eviction {}", name, cache.size()); LOGGER.trace("{}: cache size after eviction {}", name, cache.size());
final Instant nextEvictionTime = lastAccessTime.equals(Instant.MAX) ? Instant.MAX final long nextEvictionTime = lastAccessTime == Long.MAX_VALUE ? Long.MAX_VALUE
: lastAccessTime.plus(timeToLive); : lastAccessTime + timeToLive.toMillis();
return nextEvictionTime; return nextEvictionTime;
} }
private static Instant min(final Instant a, final Instant b) { private long now() {
return a.isBefore(b) ? a : b;
}
private static Duration minDuration(final Duration a, final Duration b) {
return a.compareTo(b) < 0 ? a : b;
}
private Instant now() {
return now; return now;
} }
// visible for test // visible for test
void updateTime() { void updateTime() {
now = Instant.now(clock); now = clock.millis();
} }
private void touch(final K key, final Entry<V> entry) { private void touch(final K key, final Entry<V> entry) {
if (entry != null) { if (entry != null) {
final Instant now = now(); final long now = now();
entry.touch(now); entry.touch(now);
} }
} }
private boolean isExpired(final Entry<V> entry, final Instant now) { private boolean isExpired(final Entry<V> entry, final long now) {
return entry.getLastAccessed().plus(timeToLive).isBefore(now); return (entry.getLastAccessed() + timeToLive.toMillis()) < now;
} }
private void handleEvent(final K key, final V value) { private void handleEvent(final K key, final V value) {

View File

@@ -30,16 +30,16 @@ public class HotEntryCacheTest {
@Test(invocationCount = 1) @Test(invocationCount = 1)
public void testRemovalListenerCalledOnExpire() throws InterruptedException { public void testRemovalListenerCalledOnExpire() throws InterruptedException {
final Duration originalMinSleepPeriod = HotEntryCache.getMinSleepPeriod(); final long originalMinSleepPeriod = HotEntryCache.getMinSleepPeriod();
final Duration originalMaxSleepPeriod = HotEntryCache.getMaxSleepPeriod(); final long originalMaxSleepPeriod = HotEntryCache.getMaxSleepPeriod();
try { try {
final String key = "key"; final String key = "key";
final String value = "value"; final String value = "value";
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofMillis(1), "cache-" + ++cacheId); final HotEntryCache<String, String> cache = new HotEntryCache<>(Duration.ofMillis(1), "cache-" + ++cacheId);
HotEntryCache.setMinSleepPeriod(Duration.ofMillis(1)); HotEntryCache.setMinSleepPeriod(1);
HotEntryCache.setMaxSleepPeriod(Duration.ofMillis(10)); HotEntryCache.setMaxSleepPeriod(10);
cache.addListener((k, v) -> { cache.addListener((k, v) -> {
Assert.assertEquals(k, key); Assert.assertEquals(k, key);
Assert.assertEquals(v, value); Assert.assertEquals(v, value);