All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.chrisdavenport.mules.MemoryCache.scala Maven / Gradle / Ivy

The newest version!
package io.chrisdavenport.mules

import cats._
import cats.effect._
import cats.effect.syntax.all._
import cats.syntax.all._
import scala.collection.immutable.Map

import cats.effect.std.MapRef
import cats.effect.std.syntax.all._

final class MemoryCache[F[_], K, V] private[MemoryCache] (
  private val mapRef: MapRef[F, K, Option[MemoryCache.MemoryCacheItem[V]]],
  private val purgeExpiredEntriesOpt : Option[Long => F[List[K]]], // Optional Performance Improvement over Default
  val defaultExpiration: Option[TimeSpec],
  private val onInsert: (K, V) => F[Unit],
  private val onCacheHit: (K, V) => F[Unit],
  private val onCacheMiss: K => F[Unit],
  private val onDelete: K => F[Unit]
)(implicit val F: Temporal[F]) extends Cache[F, K, V] {
  import MemoryCache.MemoryCacheItem
  private val noneF: F[None.type] = Applicative[F].pure(None)
  private def noneFA[A]: F[Option[A]] = noneF.asInstanceOf[F[Option[A]]]

  val purgeExpiredEntries: Long => F[List[K]] =
    purgeExpiredEntriesOpt.getOrElse({(_: Long) => List.empty[K].pure[F]})

  /**
   * Delete an item from the cache. Won't do anything if the item is not present.
   **/
  def delete(k: K): F[Unit] =
    mapRef.unsetKey(k) >> onDelete(k)

  /**
   * Insert an item in the cache, using the default expiration value of the cache.
   */
  def insert(k: K, v: V): F[Unit] =
    insertWithTimeout(defaultExpiration)(k, v)

  /**
   * Insert an item in the cache, with an explicit expiration value.
   *
   * If the expiration value is None, the item will never expire. The default expiration value of the cache is ignored.
   *
   * The expiration value is relative to the current clockMonotonic time, i.e. it will be automatically added to the result of clockMonotonic for the supplied unit.
   **/
  def insertWithTimeout(optionTimeout: Option[TimeSpec])(k: K, v: V): F[Unit] = {
    for {
      now <- Clock[F].monotonic
      timeout = optionTimeout.map(ts => TimeSpec.unsafeFromNanos(now.toNanos + ts.nanos))
      _ <- mapRef.setKeyValue(k, MemoryCacheItem[V](v, timeout))
      _ <- onInsert(k, v)
    } yield ()
  }

  /**
   * Return all keys present in the cache, including expired items.
   **/
  // def keys: F[Chains[K]] = key
    // ref.get.map(_.keys.toList)

  /**
   * Lookup an item with the given key, and delete it if it is expired.
   *
   * The function will only return a value if it is present in the cache and if the item is not expired.
   *
   * The function will eagerly delete the item from the cache if it is expired.
   **/
  def lookup(k: K): F[Option[V]] = {
    Clock[F].monotonic
      .flatMap{now =>
        mapRef(k).modify[F[Option[MemoryCacheItem[V]]]]{
          case s@Some(value) =>
            if (MemoryCache.isExpired(now.toNanos, value)){
              (None, onDelete(k).as(None))
            } else {
              (s, F.pure(s))
            }
          case None =>
            (None, noneFA)
        }
      }
      .flatten
      .map(_.map(_.item))
      .flatMap{
        case s@Some(v) => onCacheHit(k, v).as(s)
        case None => onCacheMiss(k).as(None)
      }
  }

  /**
   * Lookup an item with the given key, but don't delete it if it is expired.
   *
   * The function will only return a value if it is present in the cache and if the item is not expired.
   *
   * The function will not delete the item from the cache.
   **/
  def lookupNoUpdate(k: K): F[Option[V]] =
    Clock[F].monotonic
      .flatMap{now =>
        mapRef(k).get.map(
          _.flatMap(ci =>
            Alternative[Option].guard(
              !MemoryCache.isExpired(now.toNanos, ci)
            ).as(ci)
          )
        )
      }
      .map(_.map(_.item))
      .flatMap{
        case s@Some(v) => onCacheHit(k,v).as(s)
        case None => onCacheMiss(k).as(None)
      }

  /**
   * Change the default expiration value of newly added cache items. Shares an underlying reference
   * with the other cache. Use copyMemoryCache if you want different caches.
   **/
  def setDefaultExpiration(defaultExpiration: Option[TimeSpec]): MemoryCache[F, K, V] =
    new MemoryCache[F, K, V](
      mapRef,
      purgeExpiredEntriesOpt,
      defaultExpiration,
      onInsert,
      onCacheHit,
      onCacheMiss,
      onDelete
    )

  /**
   * Reference to this MemoryCache with the `onCacheHit` effect being the new function.
   */
  def setOnCacheHit(onCacheHitNew: (K, V) => F[Unit]): MemoryCache[F, K, V] =
    new MemoryCache[F, K, V](
      mapRef,
      purgeExpiredEntriesOpt,
      defaultExpiration,
      onInsert,
      onCacheHitNew,
      onCacheMiss,
      onDelete
    )

  /**
   * Reference to this MemoryCache with the `onCacheMiss` effect being the new function.
   */
  def setOnCacheMiss(onCacheMissNew: K => F[Unit]): MemoryCache[F, K, V] =
    new MemoryCache[F, K, V](
      mapRef,
      purgeExpiredEntriesOpt,
      defaultExpiration,
      onInsert,
      onCacheHit,
      onCacheMissNew,
      onDelete
    )
  /**
   * Reference to this MemoryCache with the `onDelete` effect being the new function.
   */
  def setOnDelete(onDeleteNew: K => F[Unit]): MemoryCache[F, K, V] =
    new MemoryCache[F, K, V](
      mapRef,
      purgeExpiredEntriesOpt,
      defaultExpiration,
      onInsert,
      onCacheHit,
      onCacheMiss,
      onDeleteNew
    )

  /**
   * Reference to this MemoryCache with the `onInsert` effect being the new function.
   */
  def setOnInsert(onInsertNew: (K, V) => F[Unit]): MemoryCache[F, K, V] =
    new MemoryCache[F, K, V](
      mapRef,
      purgeExpiredEntriesOpt,
      defaultExpiration,
      onInsertNew,
      onCacheHit,
      onCacheMiss,
      onDelete
    )

  /**
   * Delete all items that are expired.
   *
   * This is one big atomic operation.
   **/
  def purgeExpired: F[Unit] = {
    for {
      now <- Clock[F].monotonic
      out <- purgeExpiredEntries(now.toNanos)
      _ <-  out.traverse_(onDelete)
    } yield ()
  }

  /**
   * Reference to this MemoryCache with the `onCacheHit` effect being composed of the old and new function.
   */
  def withOnCacheHit(onCacheHitNew: (K, V) => F[Unit]): MemoryCache[F, K, V] =
    new MemoryCache[F, K, V](
      mapRef,
      purgeExpiredEntriesOpt,
      defaultExpiration,
      onInsert,
      {(k, v) => onCacheHit(k, v) >> onCacheHitNew(k, v)},
      onCacheMiss,
      onDelete
    )

  /**
   * Reference to this MemoryCache with the `onCacheMiss` effect being composed of the old and new function.
   */
  def withOnCacheMiss(onCacheMissNew: K => F[Unit]): MemoryCache[F, K, V] =
    new MemoryCache[F, K, V](
      mapRef,
      purgeExpiredEntriesOpt,
      defaultExpiration,
      onInsert,
      onCacheHit,
      {k => onCacheMiss(k) >> onCacheMissNew(k)},
      onDelete
    )

  /**
   * Reference to this MemoryCache with the `onDelete` effect being composed of the old and new function.
   */
  def withOnDelete(onDeleteNew: K => F[Unit]): MemoryCache[F, K, V] =
    new MemoryCache[F, K, V](
      mapRef,
      purgeExpiredEntriesOpt,
      defaultExpiration,
      onInsert,
      onCacheHit,
      onCacheMiss,
      {k => onDelete(k) >> onDeleteNew(k)}
    )

  /**
   * Reference to this MemoryCache with the `onInsert` effect being composed of the old and new function.
   */
  def withOnInsert(onInsertNew: (K, V) => F[Unit]): MemoryCache[F, K, V] =
    new MemoryCache[F, K, V](
      mapRef,
      purgeExpiredEntriesOpt,
      defaultExpiration,
      {(k, v) => onInsert(k, v) >> onInsertNew(k, v)},
      onCacheHit,
      onCacheMiss,
      onDelete
    )

}

object MemoryCache {
  case class MemoryCacheItem[A](
    item: A,
    itemExpiration: Option[TimeSpec]
  )

  /**
   *
   * Initiates a background process that checks for expirations every certain amount of time.
   *
   * @param memoryCache: The cache to check and expire automatically.
   * @param checkOnExpirationsEvery: How often the expiration process should check for expired keys.
   *
   * @return an `Resource[F, Unit]` that will keep removing expired entries in the background.
   **/
  def liftToAuto[F[_]: Temporal, K, V](
    memoryCache: MemoryCache[F, K, V],
    checkOnExpirationsEvery: TimeSpec
  ): Resource[F, Unit] = {
    def runExpiration(cache: MemoryCache[F, K, V]): F[Unit] = {
      val check = TimeSpec.toDuration(checkOnExpirationsEvery)
      Temporal[F].sleep(check) >> cache.purgeExpired >> runExpiration(cache)
    }

    Resource.make(runExpiration(memoryCache).start)(_.cancel).void
  }

  /**
    * Cache construction is Synchronous
    * 
    * Otherwise a copy paste of {@link #ofSingleImmutableMap() ofSingleImmutableMap}
    * 
    **/
  def inSingleImmutableMap[G[_] : Sync, F[_]: Async, K, V](
    defaultExpiration: Option[TimeSpec]
  ): G[MemoryCache[F, K, V]] = 
    Ref.in[G, F, Map[K, MemoryCacheItem[V]]](Map.empty[K, MemoryCacheItem[V]])
    .map(ref => new MemoryCache[F, K, V](
        MapRef.fromSingleImmutableMapRef(ref),
        {(l: Long) => SingleRef.purgeExpiredEntries[F,K, MemoryCacheItem[V]](ref, isExpired)(l)}.some,
        defaultExpiration,
        {(_, _) => Concurrent[F].unit},
        {(_, _) =>  Concurrent[F].unit},
        {(_: K) =>  Concurrent[F].unit},
        {(_: K) =>  Concurrent[F].unit}
    ))
  

  /**
    * Create a new cache with a default expiration value for newly added cache items.
    *
    * Items that are added to the cache without an explicit expiration value (using insert) will be inserted with the default expiration value.
    *
    * If the specified default expiration value is None, items inserted by insert will never expire.
    **/
  def ofSingleImmutableMap[F[_]: Temporal, K, V](
    defaultExpiration: Option[TimeSpec]
  ): F[MemoryCache[F, K, V]] =
    Ref.of[F, Map[K, MemoryCacheItem[V]]](Map.empty[K, MemoryCacheItem[V]])
      .map(ref => new MemoryCache[F, K, V](
        MapRef.fromSingleImmutableMapRef(ref),
        {(l: Long) => SingleRef.purgeExpiredEntries[F,K, MemoryCacheItem[V]](ref, isExpired)(l)}.some,
        defaultExpiration,
        {(_, _) => Concurrent[F].unit},
        {(_, _) =>  Concurrent[F].unit},
        {(_: K) =>  Concurrent[F].unit},
        {(_: K) =>  Concurrent[F].unit}
      ))

  def ofShardedImmutableMap[F[_]: Temporal, K, V](
    shardCount: Int,
    defaultExpiration: Option[TimeSpec]
  ): F[MemoryCache[F, K, V]] =
    PurgeableMapRef.ofShardedImmutableMap[F, K, MemoryCacheItem[V]](shardCount, isExpired).map{ smr =>
      new MemoryCache[F, K, V](
        smr.mapRef,
        Some(smr.purgeExpiredEntries),
        defaultExpiration,
        {(_, _) => Concurrent[F].unit},
        {(_, _) => Concurrent[F].unit},
        {(_: K) => Applicative[F].unit},
        {(_: K) => Applicative[F].unit}
      )
    }

  def ofConcurrentHashMap[F[_]: Async, K, V](
    defaultExpiration: Option[TimeSpec],
    initialCapacity: Int = 16,
    loadFactor: Float = 0.75f,
    concurrencyLevel: Int = 16,
  ): F[MemoryCache[F, K, V]] =
    PurgeableMapRef.ofConcurrentHashMap[F,K, MemoryCacheItem[V]](
      initialCapacity,
      loadFactor,
      concurrencyLevel,
      isExpired).map {pmr =>
      new MemoryCache[F, K, V](
          pmr.mapRef,
          Some(pmr.purgeExpiredEntries),
          defaultExpiration,
          {(_, _) => Applicative[F].unit},
          {(_, _) => Applicative[F].unit},
          {(_: K) => Applicative[F].unit},
          {(_: K) => Applicative[F].unit}
        )
  }

  def ofMapRef[F[_]: Temporal, K, V](
    mr: MapRef[F, K, Option[MemoryCacheItem[V]]],
    defaultExpiration: Option[TimeSpec]
  ): MemoryCache[F, K, V] = {
    new MemoryCache[F, K, V](
        mr,
        None,
        defaultExpiration,
        {(_, _) => Applicative[F].unit},
        {(_, _) => Applicative[F].unit},
        {(_: K) => Applicative[F].unit},
        {(_: K) => Applicative[F].unit}
      )
  }

  private def isExpired[A](checkAgainst: Long, cacheItem: MemoryCacheItem[A]): Boolean = {
    cacheItem.itemExpiration match{
      case Some(e) if e.nanos < checkAgainst => true
      case _ => false
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy