TTLCache.java [src/csip/utils] Revision: Date:
/*
* $Id: 2.6+43 TTLCache.java b92e66eabb88 2022-03-30 od $
*
* This file is part of the Cloud Services Integration Platform (CSIP),
* a Model-as-a-Service framework, API and application suite.
*
* 2012-2024, OMSLab, Colorado State University.
*
* OMSLab licenses this file to you under the MIT license.
* See the LICENSE file in the project root for more information.
*/
package csip.utils;
import java.time.Duration;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* LRU + TTL cache.
*
* - The cache has a limited size/capacity - Each entry will have a ttl on put()
* - evicted: if expired or capacity has reached.
*
* @author od
*/
public class TTLCache<K, V> {
private static final long SEC_HOUR = 60 * 60;
private static final long SEC_DAY = 24 * SEC_HOUR;
private static final ZoneId DEF_TZ = ZoneId.systemDefault();
Map<K, Item<V>> m = new ConcurrentHashMap<>();
int size = 16; // default 16 elements
// check for potential expiration and eviction at 75% capacity (size)
static final float CHECK_FULL = 0.75f;
/**
* Get the number milliseconds until end of the current day. Use case: let
* cache item expire at midnight.
*
* @return the # of ms until midnight
*/
public static long untilEndOfDay() {
long now_sec = LocalTime.now(DEF_TZ).toSecondOfDay();
return TimeUnit.MILLISECONDS.convert(SEC_DAY - now_sec, TimeUnit.SECONDS);
}
public static long untilEndOfHour() {
LocalTime now = LocalTime.now(DEF_TZ);
long now_sec = now.getMinute() * 60 + now.getSecond();
return TimeUnit.MILLISECONDS.convert(SEC_HOUR - now_sec, TimeUnit.SECONDS);
}
private static class Item<V> {
// soft ttl in ms
final long soft_ttl;
// soft expiration time, will get updated if there is a get() within the ttl.
// exptime = now + ttl
long soft_exptime;
// hard expiration time. (e.g. evict for sure at that time, does not get updated
// once the Item is created. )
// hard_exptime = now + hard_ttl
final long hard_exptime;
final V value;
Item(V value, long soft_ttl, long hard_ttl) {
this.soft_ttl = soft_ttl;
this.value = value;
long now = System.currentTimeMillis();
soft_exptime = expire(now, soft_ttl);
hard_exptime = expire(now, hard_ttl);
}
void updateExpiration(long time) {
soft_exptime = expire(time, soft_ttl);
}
boolean isExpired(long systime) {
if (systime > hard_exptime)
return true;
return systime > soft_exptime;
}
@Override
public String toString() {
return value.toString() + "[" + soft_exptime + "]";
}
/**
*
* @param systime
* @param ttl -1 means no expiration
* @return
*/
private static long expire(long systime, long ttl) {
return ttl < 0 ? Long.MAX_VALUE : (systime + ttl);
}
}
/**
* The size of the cache
*
* @param size the new size of the cache
* @return this TTLCache instance
*/
public TTLCache withSize(int size) {
if (size < 0)
throw new IllegalArgumentException("size < 0!");
this.size = size;
synchronized (this) {
removeAllExpired();
// evict if needed until cache is not full anymore.
evict();
}
return this;
}
public V put(K key, V value, String soft_ttl) {
return put(key, value, soft_ttl, -1);
}
public V put(K key, V value, String soft_ttl, long hard_ttl) {
long soft_ttl_ms = Duration.parse(soft_ttl).getSeconds() * 1000;
return put(key, value, soft_ttl_ms, hard_ttl);
}
/**
* Puts an entry into the cache.
*
* @param key the key
* @param value the val
* @param soft_ttl the ttl in ms
* @return the previous value
*/
public V put(K key, V value, long soft_ttl) {
return put(key, value, soft_ttl, -1);
}
public V put(K key, V value, long soft_ttl, long hard_ttl) {
if (size == 0 || key == null)
return null;
return put(key, new Item<V>(value, soft_ttl, hard_ttl));
}
private V put(K k, Item<V> i) {
Item<V> prev = m.put(k, i);
if (m.size() > size * CHECK_FULL) {
synchronized (this) {
removeAllExpired();
// evict if needed until cache is not full anymore.
evict();
}
}
return prev == null ? null : prev.value;
}
/**
* Get an entry from the cache. If expired (hard & soft) then it returns null.
* If not soft expired, it resets the ttl and returns the value.
*
* @param k the key
* @return the value if not expired or null if not present or expired.
*/
public V get(K k) {
if (size == 0 || k == null)
return null;
if (m.containsKey(k)) {
Item<V> i = m.get(k);
long now = System.currentTimeMillis();
if (i.isExpired(now)) {
// it expired, now removed.
m.remove(k);
return null;
} else {
// not expired, reset soft expiration
i.updateExpiration(now);
return i.value;
}
}
return null;
}
public void clear() {
m.clear();
}
public int getSize() {
return size;
}
private K findNextKeyToExpire() {
long time = Long.MAX_VALUE;
K k = null;
for (Map.Entry<K, Item<V>> e : m.entrySet()) {
if (e.getValue().soft_exptime < time) {
time = e.getValue().soft_exptime;
k = e.getKey();
}
}
return k;
}
private void evict() {
while (m.size() > size) {
K k = findNextKeyToExpire();
if (k == null)
return;
// evict element
m.remove(k);
}
}
private void removeAllExpired() {
long now = System.currentTimeMillis();
m.entrySet().removeIf(e -> e.getValue().isExpired(now));
}
}