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));
  }

}