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:
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user