diff --git a/build.sbt b/build.sbt index 30c7daa..8339e48 100644 --- a/build.sbt +++ b/build.sbt @@ -45,6 +45,8 @@ libraryDependencies ++= Seq( scalatest % Test ) +autoAPIMappings := true + licenses := Seq(("MIT", url("https://opensource.org/licenses/MIT"))) description := "Cache in Scala with cats-effect" diff --git a/src/main/scala/com/evolution/scache/Cache.scala b/src/main/scala/com/evolution/scache/Cache.scala index 8c31662..ff6b80d 100644 --- a/src/main/scala/com/evolution/scache/Cache.scala +++ b/src/main/scala/com/evolution/scache/Cache.scala @@ -12,88 +12,355 @@ import com.evolutiongaming.smetrics.MeasureDuration import scala.util.control.NoStackTrace +/** Tagless Final implementation of a cache interface. + * + * Most developers using the library may want to use [[Cache#expiring]] to + * construct the cache, though, if element expiration is not required, then + * it might be useful to use + * [[Cache#loading[F[_],K,V](partitions:Option[Int])*]] instead. + * + * @tparam F + * Effect to be used in effectful methods such as [[#get]]. + * @tparam K + * Key type. While there is no restriction / context bounds on this type + * parameter, the implementation is expected to abuse the fact that it is + * possible to call `hashCode` and `==` on any object in JVM. If performance + * is important, it is recommeded to limit `K` to the types where these + * operations are fast such as `String`, `Integer` or a case class. + * @tparam V + * Value type. + */ trait Cache[F[_], K, V] { type Release = F[Unit] type Released = F[Unit] + /** Gets a value for specific key. + * + * @param key + * The key to return the value for. + * @return + * - If the key is already in the cache then `F[_]` will complete to + * `Some(v)`, where `v` is a value associated with the key. + * - If the new value is loading (as result of [[#getOrUpdate]] or + * implementation-specific refresh), then `F[_]` will not complete until + * the value is fully loaded. + * - `F[_]` will complete to [[scala.None]] if there is no `key` present in + * the cache. + */ def get(key: K): F[Option[V]] + /** Gets a value for specific key. + * + * The point of this method, comparing to [[#get]] is that it does not wait + * if the value for a specific key is still loading, allowing the caller to + * not block while waiting for it. + * + * @param key + * The key to return the value for. + * @return + * - If the key is already in the cache then `F[_]` will complete to + * `Some(Right(v))`, where `v` is a value associated with the key. + * - If the new value is loading (as result of [[#getOrUpdate]] or + * implementation-specific refresh), then `F[_]` will complete to + * `Some(Left(io))`, where `io` will not complete until the value is + * fully loaded. + * - `F[_]` will complete to [[scala.None]] if there is no `key` present in + * the cache. + */ def get1(key: K): F[Option[Either[F[V], V]]] - /** - * Does not run `value` concurrently for the same key + /** Gets a value for specific key, or loads it using the provided function. + * + * The method does not run `value` concurrently for the same key. I.e. if + * `value` takes a time to be completed, and [[#getOrUpdate]] is called + * several times, then the consequent calls will not cause `value` to be + * called, but will wait for the first one to complete. + * + * @param key + * The key to return the value for. + * @param value + * The function to run to load the missing value with. + * + * @return + * - If the key is already in the cache then `F[_]` will complete to the + * value associated with the key. + * - `F[_]` will complete to the value loaded by `value` function if there + * is no `key` present in the cache. + * - If the new value is loading (as result of this or another + * [[#getOrUpdate]] call, or implementation-specific refresh), then + * `F[_]` will not complete until the value is fully loaded. */ def getOrUpdate(key: K)(value: => F[V]): F[V] - /** - * Does not run `value` concurrently for the same key - * release will be called upon key removal from the cache + /** Gets a value for specific key, or loads it using the provided function. + * + * The point of this method, comparing to [[#getOrUpdate]] is that it does + * not wait if value for a key is still loading, allowing the caller to not + * block while waiting for the result. + * + * It also allows some additional functionality similar to + * [[#put(key:K,value:V,release:Option[*]]. + * + * The method does not run `value` concurrently for the same key. I.e. if + * `value` takes a time to be completed, and [[#getOrUpdate1]] is called + * several times, then the consequent calls will not cause `value` to be + * called, but will wait for the first one to complete. + * + * The `value` is only called if `key` is not found, the tuple elements will + * be used like following: + * - `A` will be returned by [[#getOrUpdate1]] to differentiate from the + * case when the value is already there, + * - `V` will be put to the cache, + * - `Release`, if present, will be called when this value is removed from + * the cache. * - * @return either A passed as argument or `Either[F[V], V]` that represents loading or loaded value + * Note: this method is meant to be used where [[cats.effect.Resource]] is + * not convenient to use, i.e. when integration with legacy code is required + * or for internal implementation. For all other cases it is recommended to + * use [[Cache.CacheOps#getOrUpdateResource]] instead as more human-readable + * alternative. + * + * @param key + * The key to return the value for. + * @param value + * The function to run to load the missing value with. + * + * @tparam A + * Arbitrary type of a value to return in case key was not present in a + * cache. + * + * @return + * Either `A` passed as argument, if `key` was not found in cache, or + * `Either[F[V], V]` that represents loading or loaded value, if `key` is + * already in the cache. */ def getOrUpdate1[A](key: K)(value: => F[(A, V, Option[Release])]): F[Either[A, Either[F[V], V]]] - /** - * Does not run `value` concurrently for the same key - * In case of none returned, value will be ignored by cache + /** Gets a value for specific key, or tries to load it. + * + * The difference between this method and [[#getOrUpdate]] is that this one + * allows the loading function to fail finding the value, i.e. return + * [[scala.None]]. + * + * @param key + * The key to return the value for. + * @param value + * The function to run to load the missing value with. + * + * @return + * The same semantics applies as in [[#getOrUpdate]], except that the + * method may return [[scala.None]] in case `value` completes to + * [[scala.None]]. */ def getOrUpdateOpt(key: K)(value: => F[Option[V]]): F[Option[V]] - /** - * @return previous value if any, possibly not yet loaded + /** Puts a value into cache under specific key. + * + * If the value is already being loaded (using [[#getOrUpdate]] method?) then + * the returned `F[_]` will wait for it to fully load, and then overwrite it. + * + * @param key + * The key to store value for. + * @param value + * The new value to put into the cache. + * + * @return + * A previous value is returned if it was already added into the cache, + * [[scala.None]] otherwise. The returned value is wrapped into `F[_]` + * twice, because outer `F[_]` will complete when the value is put into + * cache, but the second when `release` function passed to + * [[#put(key:K,value:V,release:Cache*]] completes, i.e. the underlying + * resource is fully released. */ def put(key: K, value: V): F[F[Option[V]]] - /** - * @return previous value if any, possibly not yet loaded + /** Puts a value into cache under specific key. + * + * If the value is already being loaded (using [[#getOrUpdate]] method?) then + * the returned `F[_]` will wait for it to fully load, and then overwrite it. + * + * @param key + * The key to store value for. + * @param value + * The new value to put into the cache. + * @param release + * The function to call when the value is removed from the cache. + * + * @return + * A previous value is returned if it was already added into the cache, + * [[scala.None]] otherwise. The returned value is wrapped into `F[_]` + * twice, because outer `F[_]` will complete when the value is put into + * cache, but the second when `release` function passed to + * [[#put(key:K,value:V,release:Cache*]] completes, i.e. the underlying + * resource is fully released. */ def put(key: K, value: V, release: Release): F[F[Option[V]]] - /** - * @return previous value if any, possibly not yet loaded + /** Puts a value into cache under specific key. + * + * If the value is already being loaded (using [[#getOrUpdate]] method?) then + * the returned `F[_]` will wait for it to fully load, and then overwrite it. + * + * @param key + * The key to store value for. + * @param value + * The new value to put into the cache. + * @param release + * The function to call when the value is removed from the cache. + * No function will be called if it is set to [[scala.None]]. + * + * @return + * A previous value is returned if it was already added into the cache, + * [[scala.None]] otherwise. The returned value is wrapped into `F[_]` + * twice, because outer `F[_]` will complete when the value is put into + * cache, but the second when `release` function passed to + * [[#put(key:K,value:V,release:Cache*]] completes, i.e. the underlying + * resource is fully released. */ def put(key: K, value: V, release: Option[Release]): F[F[Option[V]]] - + /** Checks if the value for the key is present in the cache. + * + * @return + * `true` if either loaded or loading value is present in the cache. + */ def contains(key: K): F[Boolean] - + /** Calculates the size of the cache including both loaded and loading keys. + * + * May iterate over all of keys for map-bazed implementation, hence should be + * used with care on very large caches. + * + * @return + * current size of the cache. + */ def size: F[Int] - + /** Returns set of the keys present in the cache, either loaded or loading. + * + * @return + * keys present in the cache, either loaded or loading. + */ def keys: F[Set[K]] - /** - * Might be an expensive call + /** Returns map representation of the cache. + * + * Warning: this might be an expensive call to make. + * + * @return + * All keys and values in the cache put into map. Both loaded and loading + * values will be wrapped into `F[V]`. */ def values: F[Map[K, F[V]]] - /** - * @return either map of either loading or loaded value - * Might be an expensive call + /** Returns map representation of the cache. + * + * The different between this method and [[#values]] is that loading values + * are not wrapped into `F[V]` and decision if it is worth to wait for their + * completion is left to the caller discretion. + * + * Warning: this might be an expensive call to make. + * + * @return + * All keys and values in the cache put into map. Loaded values are + * returned as `Right(v)`, while loading ones are represented by + * `Left(F[V])`. */ def values1: F[Map[K, Either[F[V], V]]] - /** - * @return previous value if any, possibly not yet loaded + /** Removes a key from the cache, and also calls a release function. + * + * @return + * A stored value is returned if such was present in the cache, + * [[scala.None]] otherwise. The returned value is wrapped into `F[_]` + * twice, because outer `F[_]` will complete when the value is put into + * cache, but the second when `release` function passed to + * [[#put(key:K,value:V,release:Cache*]] completes, i.e. the underlying + * resource is fully released. */ def remove(key: K): F[F[Option[V]]] - - /** - * Removes loading values from the cache, however does not cancel them + /** Removes all the keys and their respective values from the cache. + * + * Both loaded and loading values are removed, and `release` function is + * called on them if present. The call does not cancel the loading values, + * but waits until these are fully loaded, instead. + * + * @return + * The returned `Unit` is wrapped into `F[_]` twice, because outer + * `F[Released]` will complete when the value is put into cache, but the + * second `Released = F[Unit]` when `release` function passed to + * [[#put(key:K,value:V,release:Cache*]] completes, i.e. the underlying + * resource is fully released. */ def clear: F[Released] + /** Aggregate all keys and values present in the cache to something else. + * + * Example: calculate sum of all loaded [[scala.Int]] values: + * {{{ + * cache.foldMap { + * case (key, Right(loadedValue)) => loadedValue.pure[F] + * case (key, Left(pendingValue)) => 0.pure[F] + * } + * }}} + * + * @tparam A + * Type to map the key/values to, and aggregate with. It requires + * [[cats.kernel.CommutativeMonoid]] to be present to be able to sum up the + * values, without having a guarantee about the order of the values being + * aggregates as the order may be random depending on a cache + * implementation. + * + * @return + * Result of the aggregation, i.e. all mapped values combined using passed + * [[cats.kernel.CommutativeMonoid]]. + */ def foldMap[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]): F[A] + /** Aggregate all keys and values present in the cache to something else. + * + * The difference between this method and [[#foldMap]] is that this one may + * perform the work in parallel. + * + * Example: calculate sum of all loading [[scala.Int]] values in parallel: + * {{{ + * cache.foldMapPar { + * case (key, Right(loadedValue)) => 0.pure[F] + * case (key, Left(pendingValue)) => pendingValue + * } + * }}} + * + * @tparam A + * Type to map the key/values to, and aggregate with. It requires + * [[cats.kernel.CommutativeMonoid]] to be present to be able to sum up the + * values, without having a guarantee about the order of the values being + * aggregates as the order may be random depending on a cache + * implementation. + * + * @return + * Result of the aggregation, i.e. all mapped values combined using passed + * [[cats.kernel.CommutativeMonoid]]. + */ def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]): F[A] } object Cache { + /** Creates an always-empty implementation of cache. + * + * The implementation *almost* always returns [[scala.None]] regardess the + * key. The notable exception are [[Cache#getOrUpdate]], + * [[Cache#getOrUpdate1]] and [[Cache#getOrUpdateOpt]] methods, which return + * the value passed to them to ensure the consistent behavior (i.e. it could + * be a suprise if someone calls [[Cache#getOrUpdateOpt]] with [[scala.Some]] + * and gets [[scala.None]] as a result). + * + * It is meant to be used in tests, or as a stub in the code where cache + * should be disabled. + */ def empty[F[_]: Monad, K, V]: Cache[F, K, V] = { abstract class Empty extends Cache.Abstract0[F, K, V] @@ -133,14 +400,96 @@ object Cache { } } + /** Creates a cache implementation, which is able to load the missing values. + * + * Same as [[[[#loading[F[_],K,V](partitions:Int)*]]]], but with number of + * paritions determined automatically using passed `Runtime` implementation. + * + * Minimal usage example: + * {{{ + * Cache.loading[F, String, User] + * }}} + * + * @return + * A new instance of a cache wrapped into [[cats.effect.Resource]]. Note, + * that [[Cache#clear]] method will be called on underlying cache when + * resource is released to make sure all resources stored in a cache are + * also released. + */ def loading[F[_]: Concurrent: Parallel: Runtime, K, V]: Resource[F, Cache[F, K, V]] = { loading(none) } + /** Creates a cache implementation, which is able to load the missing values. + * + * Same as [[#loading[F[_],K,V](partitions:Option[Int])*]], but without the + * need to use `Option`. + * + * Minimal usage example: + * {{{ + * Cache.loading[F, String, User](partitions = 8) + * }}} + * + * @return + * A new instance of a cache wrapped into [[cats.effect.Resource]]. Note, + * that [[Cache#clear]] method will be called on underlying cache when + * resource is released to make sure all resources stored in a cache are + * also released. + */ def loading[F[_]: Concurrent: Parallel: Runtime, K, V](partitions: Int): Resource[F, Cache[F, K, V]] = { loading(partitions.some) } + /** Creates a cache implementation, which is able to load the missing values. + * + * To speed the operations, the cache may use several partitions each of whom + * may be accessed in parallel. + * + * Note, that the values getting into this cache never expire, i.e. the cache + * will grow indefinetely unless [[Cache#remove]] is called. See + * [[#expiring]] for the implementation, which allows automatic expriation of + * the values. + * + * Here is a short description of why some of the context bounds are required + * on `F[_]`: + * - [[cats.Parallel]] is required for an efficient [[Cache#foldMapPar]] + * implementation whenever cache partitioning is used. Cache partitioning + * itself allows splitting underlying cache into multiple partitions, so + * there is no contention on a single [[cats.effect.Ref]] when cache need + * to be updated. + * - `Runtime` is used to determine optimal number of partitions based on + * CPU count if the value is not provided as a parameter. + * - [[cats.effect.Sync]] (which comes as part of + * [[cats.effect.Concurrent]]), allows internal structures using + * [[cats.effect.Ref]] and [[cats.effect.Deferred]] to be created. + * - [[cats.effect.Concurrent]], allows `release` parameter in + * [[Cache#put(key:K,value:V,release:Cache*]] and [[Cache#getOrUpdate1]] + * methods to be called in background without waiting for release to be + * completed. + * + * Minimal usage example: + * {{{ + * Cache.loading[F, String, User](partitions = None) + * }}} + * + * @tparam F + * Effect type. See [[Cache]] for more details. + * @tparam K + * Key type. See [[Cache]] for more details. + * @tparam V + * Value type. See [[Cache]] for more details. + * + * @param partitions + * Number of partitions to use, or [[scala.None]] in case number of + * partitions should be determined automatically using passed `Runtime` + * implementation. + * + * @return + * A new instance of a cache wrapped into [[cats.effect.Resource]]. Note, + * that [[Cache#clear]] method will be called on underlying cache when + * resource is released to make sure all resources stored in a cache are + * also released. + */ def loading[F[_]: Concurrent: Parallel: Runtime, K, V](partitions: Option[Int] = None): Resource[F, Cache[F, K, V]] = { implicit val hash: Hash[K] = Hash.fromUniversalHashCode[K] @@ -158,6 +507,53 @@ object Cache { result.breakFlatMapChain } + /** Creates a cache implementation, which is able remove the stale values. + * + * The underlying storage implementation is the same as in + * [[#loading[F[_],K,V](partitions:Option[Int])*]], but the expiration + * routines are added on top of it. + * + * Besides a value expiration leading to specific key being removed from the + * cache, the implementation is capable of _refreshing_ the values instead of + * removing them, which might be useful if the cache is used as a wrapper for + * setting or configuration service. The feature is possible to configure + * using `config` parameter. + * + * In adddition to context bounds used in + * [[#loading[F[_],K,V](partitions:Option[Int])*]], this implementation also + * adds [[cats.effect.Clock]] (as part of [[cats.effect.Temporal]]), to have + * the ability to schedule cache clean up in a concurrent way. + * + * Minimal usage example: + * {{{ + * Cache.expiring[F, String, User]( + * config = ExpiringCache.Config(expireAfterRead = 1.minute), + * partitions = None, + * ) + * }}} + * + * @tparam F + * Effect type. See [[#loading[F[_],K,V](partitions:Option[Int])*]] and + * [[Cache]] for more details. + * @tparam K + * Key type. See [[Cache]] for more details. + * @tparam V + * Value type. See [[Cache]] for more details. + * + * @param config + * Cache configuration. See [[ExpiringCache.Config]] for more details on + * what parameters could be configured. + * @param partitions + * Number of partitions to use, or [[scala.None]] in case number of + * partitions should be determined automatically using passed `Runtime` + * implementation. + * + * @return + * A new instance of a cache wrapped into [[cats.effect.Resource]]. Note, + * that [[Cache#clear]] method will be called on underlying cache when + * resource is released to make sure all resources stored in a cache are + * also released. + */ def expiring[F[_]: Temporal: Runtime: Parallel, K, V]( config: ExpiringCache.Config[F, K, V], partitions: Option[Int] = None @@ -186,6 +582,26 @@ object Cache { result.breakFlatMapChain } + /** Creates [[Cache]] interface to a set of precreated caches. + * + * This method is required to use common partitioning implementation for + * various caches and is not intended to be called directly. Cache + * partitioning itself allows splitting underlying cache into multiple + * partitions, so there is no contention on a single [[cats.effect.Ref]] when + * cache need to be updated. + * + * It is only left public for sake of backwards compatibility. + * + * Please consider using either + * [[#loading[F[_],K,V](partitions:Option[Int])*]] or [[#expiring]] instead. + * + * Here is a short description of why some of the context bounds are required + * on `F[_]`: + * - [[cats.MonadError]] is required to throw an error in case partitioning + * function passed as part of [[Partitions]] does not return any values. + * - [[cats.Parallel]] is required for an efficient [[Cache#foldMapPar]] + * implementation. + */ def fromPartitions[F[_]: MonadThrow: Parallel, K, V](partitions: Partitions[K, Cache[F, K, V]]): Cache[F, K, V] = { PartitionedCache(partitions) } @@ -310,8 +726,30 @@ object Cache { } } + /** Prevents adding new keys with `release` after cache itself was released. + * + * This may be useful, for example, to prevent dangling cache references to + * be filled instead of an intended instance. + */ def withFence(implicit F: Concurrent[F]): Resource[F, Cache[F, K, V]] = CacheFenced.of(self) + /** Gets a value for specific key or uses another value. + * + * The semantics is exactly the same as in [[Cache#get]]. + * + * Warning: The value passed as a second argument may only be returned, and + * never put into cache. If putting a value into cache is required, then + * [[Cache#getOrUpdate]] should be called instead. + * + * @param key + * The key to return the value for. + * @param value + * The function to run to get the missing value with. + * + * @return + * The same semantics applies as in [[Cache#getOrUpdate]], except that in + * this method there is no possibility to get [[scala.None]]. + */ def getOrElse(key: K, value: => F[V])(implicit F: Monad[F]): F[V] = { self .get(key) @@ -321,6 +759,33 @@ object Cache { } } + /** Gets a value for specific key, or loads it using a specified function. + * + * The difference between this method and [[Cache#getOrUpdate1]] is that + * this one does not differentiate between loading or loaded values present + * in a cache. If the value is still loading, `F[_]` will not complete + * until is is fully loaded. + * + * Also this method is meant to be used where [[cats.effect.Resource]] is + * not convenient to use, i.e. when integration with legacy code is + * required or for internal implementation. For all other cases it is + * recommended to use [[#getOrUpdateResource]] instead as more + * human-readable alternative. + * + * @param key + * The key to return the value for. + * @param value + * The function to run to load the missing value with. + * + * @tparam A + * Arbitrary type of a value to return in case key was not present in a + * cache. + * + * @return + * The same semantics applies as in [[Cache#getOrUpdate1]], except that + * in this method `F[_]` will only complete when the value is fully + * loaded. + */ def getOrUpdate2[A]( key: K)( value: => F[(A, V, Option[Cache[F, K, V]#Release])])(implicit @@ -335,10 +800,32 @@ object Cache { } } - /** - * Does not run `value` concurrently for the same key - * release will be called upon key removal from the cache - * In case of none returned, value will be ignored by cache + /** Gets a value for specific key, or tries to load it. + * + * The difference between this method and [[Cache#getOrUpdate1]] is that + * this one allows the loading function to fail finding the value, i.e. + * return [[scala.None]]. + * + * + * Also this method is meant to be used where [[cats.effect.Resource]] is + * not convenient to use, i.e. when integration with legacy code is + * required or for internal implementation. For all other cases it is + * recommended to use [[#getOrUpdateResourceOpt]] instead as more + * human-readable alternative. + * + * @param key + * The key to return the value for. + * @param value + * The function to run to load the missing value with. + * + * @tparam A + * Arbitrary type of a value to return in case key was not present in a + * cache. + * + * @return + * The same semantics applies as in [[Cache#getOrUpdate1]], except that + * the method may return [[scala.None]] in case `value` completes to + * [[scala.None]]. */ def getOrUpdateOpt1[A](key: K)( value: => F[Option[(A, V, Option[Cache[F, K, V]#Release])]])(implicit @@ -355,9 +842,30 @@ object Cache { .recover { case NoneError => none } } - /** - * Does not run `value` concurrently for the same key - * Resource will be release upon key removal from the cache + /** Gets a value for specific key, or loads it using the provided function. + * + * The difference between this method and [[Cache#getOrUpdate]] is that it + * accepts [[cats.effect.Resource]] as a value parameter and releases it + * when the value is removed from cache. + * + * The method does not run `value` concurrently for the same key. I.e. if + * `value` takes a time to be completed, and [[#getOrUpdateResource]] is + * called several times, then the consequent calls will not cause `value` + * to be called, but will wait for the first one to complete. + * + * @param key + * The key to return the value for. + * @param value + * The function to run to load the missing resource with. + * + * @return + * - If the key is already in the cache then `F[_]` will complete to the + * value associated with the key. + * - `F[_]` will complete to the value loaded by `value` function if + * there is no `key` present in the cache. + * - If the new value is loading (as result of this or another + * [[#getOrUpdateResource]] call, or implementation-specific refresh), + * then `F[_]` will not complete until the value is fully loaded. */ def getOrUpdateResource(key: K)(value: => Resource[F, V])(implicit F: MonadCancel[F, Throwable]): F[V] = { self @@ -373,10 +881,22 @@ object Cache { } } - /** - * Does not run `value` concurrently for the same key - * Resource will be release upon key removal from the cache - * In case of none returned, value will be ignored by cache + /** Gets a value for specific key, or tries to load it. + * + * The difference between this method and [[#getOrUpdateResource]] is + * that this one allows the loading function to fail finding the value, + * i.e. return [[scala.None]]. + * + * @param key + * The key to return the value for. + * @param value + * The function to run to load the missing value with. + * + * @return + * The same semantics applies as in [[#getOrUpdateResource]], except that + * the method may return [[scala.None]] in case `value` completes to + * [[scala.None]]. The resource will be released normally even if `None` + * is returned. */ def getOrUpdateResourceOpt(key: K)(value: => Resource[F, Option[V]])(implicit F: MonadCancel[F, Throwable]): F[Option[V]] = { self @@ -400,4 +920,4 @@ object Cache { } } } -} \ No newline at end of file +} diff --git a/src/main/scala/com/evolution/scache/ExpiringCache.scala b/src/main/scala/com/evolution/scache/ExpiringCache.scala index 8b671de..6ef8d22 100644 --- a/src/main/scala/com/evolution/scache/ExpiringCache.scala +++ b/src/main/scala/com/evolution/scache/ExpiringCache.scala @@ -17,7 +17,7 @@ object ExpiringCache { private[scache] def of[F[_], K, V]( config: Config[F, K, V] )(implicit G: Temporal[F]): Resource[F, Cache[F, K, V]] = { - + type E = Entry[V] val cooldown = config.expireAfterRead.toMillis / 5 @@ -330,7 +330,29 @@ object ExpiringCache { def touched: Timestamp = read.getOrElse(created) } - + + /** Configuration of a refresh background job. + * + * Usage example (`SettingService.get` returns `F[Option[Setting]]`): + * {{{ + * ExpiringCache.Refresh( + * interval = 1.minute, + * value = key => SettingService.getOrNone(key) + * ) + * }}} + * + * @param interval + * How often the refresh routine should be called. Note, that all cache + * entries will be refreshed regardless how long ago these were added to + * the cache, hence the operation might be expensive. + * @param value + * The function which returns a value for the specific key. While the + * function itself is pure, all the current implementation use + * `Refresh[K, F[Option[T]]]`, so `V` is not a real value, but an effectful + * function which calculates a value. The [[scala.Option]] is used to + * indicate if value should be removed (i.e. [[scala.None]] means the + * key is to be deleted). + */ final case class Refresh[-K, +V](interval: FiniteDuration, value: K => V) object Refresh { @@ -343,6 +365,50 @@ object ExpiringCache { } + /** Configuration of expiring cache, including the potential refresh routine. + * + * Performance consideration: The frequency of internal expiration routine + * depends on `expireAfterRead` and `expireAfterWrite` parameters (it is + * actually done more often, for sake of faster cleanup), so the very small + * value set for any of these parameters may affect the performance of the + * cache, as cleanup will happen too often. + * + * Usage example (`SettingService.get` returns `F[Option[Setting]]`): + * {{{ + * ExpiringCache.Config( + * expireAfterRead = 1.minute, + * expireAfterWrite = None, + * maxSize = None, + * refresh = Some(ExpiringCache.Refresh( + * interval = 1.minute, + * value = key => SettingService.get(key) + * )) + * }}} + * + * @param expireAfterRead + * The value will be removed after the period set by this parameter if it + * was not read (i.e. one of methods reading the value such as + * [[Cache#get]] or [[Cache#getOrUpdate]] method was not called). Note, + * that this removal has a best effort guarantee, i.e. there is possibility + * that value is still there after it expires. + * @param expireAfterWrite + * If set to [[scala.Some]], the value will be removed after the period set + * by this parameter regardless if it was touched by [[Cache#get]] or + * similar methods. Note, that this removal has a best effort guarantee, + * i.e. there is possibility that value is still there after it expires. + * @param maxSize + * If set then the cache implementation will try to keep the cache size + * under `maxSize` whenever clean up routine happens. If the cache size + * exceeds the value, it will try to drop part of non-expired element + * sorted by the timestamp, when these elements were last read. There is + * no guarantee, though, that this size will not be exceeded a bit, if + * a lot of elements are put into cache between the cleanup calls. + * @param refresh + * If set to [[scala.Some]], the cache will schedule a background job, + * which will refresh or remove the _existing_ values regularly. The + * keys not already present in a cache will not be affected anyhow. See + * [[Refresh]] documentation for more details. + */ final case class Config[F[_], -K, V]( expireAfterRead: FiniteDuration, expireAfterWrite: Option[FiniteDuration] = None, @@ -362,4 +428,4 @@ object ExpiringCache { } } } -} \ No newline at end of file +}