diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..f81234d --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Scala Steward: Reformat with scalafmt 3.7.14 +e6c50662533357d4a7333b01256a3ecb999e3d51 diff --git a/.scalafmt.conf b/.scalafmt.conf index b8da4ca..f1962fa 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,2 +1,2 @@ -version = "3.7.4" +version = "3.7.14" runner.dialect = scala213source3 \ No newline at end of file diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c19ff95..c6a6106 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -2,14 +2,14 @@ import sbt.* object Dependencies { - val scalatest = "org.scalatest" %% "scalatest" % "3.2.16" - val `cats-helper` = "com.evolutiongaming" %% "cats-helper" % "3.6.0" - val smetrics = "com.evolutiongaming" %% "smetrics" % "2.0.0" - val `kind-projector` = "org.typelevel" % "kind-projector" % "0.13.2" - val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % "0.3.1" + val scalatest = "org.scalatest" %% "scalatest" % "3.2.16" + val `cats-helper` = "com.evolutiongaming" %% "cats-helper" % "3.6.0" + val smetrics = "com.evolutiongaming" %% "smetrics" % "2.0.0" + val `kind-projector` = "org.typelevel" % "kind-projector" % "0.13.2" + val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % "0.3.1" object Cats { - val core = "org.typelevel" %% "cats-core" % "2.9.0" + val core = "org.typelevel" %% "cats-core" % "2.9.0" val effect = "org.typelevel" %% "cats-effect" % "3.4.11" } } diff --git a/src/main/scala-2/com/evolution/scache/CacheOpsCompat.scala b/src/main/scala-2/com/evolution/scache/CacheOpsCompat.scala index 8c4aa46..5827db4 100644 --- a/src/main/scala-2/com/evolution/scache/CacheOpsCompat.scala +++ b/src/main/scala-2/com/evolution/scache/CacheOpsCompat.scala @@ -21,15 +21,16 @@ private[scache] object CacheOpsCompat { /** Gets a value for specific key, or tries to load it. * - * The difference between this method and [[com.evolution.scache.Cache#getOrUpdate1]] is that - * this one allows the loading function to fail finding the value, i.e. - * return [[scala.None]]. + * The difference between this method and + * [[com.evolution.scache.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 [[com.evolution.scache.Cache.CacheOps#getOrUpdateResourceOpt]] instead as more - * human-readable alternative. + * recommended to use + * [[com.evolution.scache.Cache.CacheOps#getOrUpdateResourceOpt]] instead + * as more human-readable alternative. * * @param key * The key to return the value for. @@ -41,9 +42,9 @@ private[scache] object CacheOpsCompat { * cache. * * @return - * The same semantics applies as in [[com.evolution.scache.Cache#getOrUpdate1]], except that - * the method may return [[scala.None]] in case `value` completes to - * [[scala.None]]. + * The same semantics applies as in + * [[com.evolution.scache.Cache#getOrUpdate1]], except that the method + * may return [[scala.None]] in case `value` completes to [[scala.None]]. */ def getOrUpdateOpt1Compat[A]( key: K @@ -63,9 +64,10 @@ private[scache] object CacheOpsCompat { /** Gets a value for specific key, or tries to load it. * - * The difference between this method and [[com.evolution.scache.Cache.CacheOps#getOrUpdateResource]] is that - * this one allows the loading function to fail finding the value, - * i.e. return [[scala.None]]. + * The difference between this method and + * [[com.evolution.scache.Cache.CacheOps#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. @@ -73,8 +75,9 @@ private[scache] object CacheOpsCompat { * The function to run to load the missing value with. * * @return - * The same semantics applies as in [[com.evolution.scache.Cache.CacheOps#getOrUpdateResource]], except that - * the method may return [[scala.None]] in case `value` completes to + * The same semantics applies as in + * [[com.evolution.scache.Cache.CacheOps#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. */ diff --git a/src/main/scala-3/com/evolution/scache/CacheOpsCompat.scala b/src/main/scala-3/com/evolution/scache/CacheOpsCompat.scala index 0900fd3..8d9ca42 100644 --- a/src/main/scala-3/com/evolution/scache/CacheOpsCompat.scala +++ b/src/main/scala-3/com/evolution/scache/CacheOpsCompat.scala @@ -20,15 +20,16 @@ private[scache] object CacheOpsCompat { /** Gets a value for specific key, or tries to load it. * - * The difference between this method and [[com.evolution.scache.Cache#getOrUpdate1]] is that - * this one allows the loading function to fail finding the value, i.e. - * return [[scala.None]]. + * The difference between this method and + * [[com.evolution.scache.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 [[com.evolution.scache.Cache.CacheOps#getOrUpdateResourceOpt]] instead as more - * human-readable alternative. + * recommended to use + * [[com.evolution.scache.Cache.CacheOps#getOrUpdateResourceOpt]] instead + * as more human-readable alternative. * * @param key * The key to return the value for. @@ -40,9 +41,9 @@ private[scache] object CacheOpsCompat { * cache. * * @return - * The same semantics applies as in [[com.evolution.scache.Cache#getOrUpdate1]], except that - * the method may return [[scala.None]] in case `value` completes to - * [[scala.None]]. + * The same semantics applies as in + * [[com.evolution.scache.Cache#getOrUpdate1]], except that the method + * may return [[scala.None]] in case `value` completes to [[scala.None]]. */ def getOrUpdateOpt1Compat[A]( key: K @@ -62,9 +63,10 @@ private[scache] object CacheOpsCompat { /** Gets a value for specific key, or tries to load it. * - * The difference between this method and [[com.evolution.scache.Cache.CacheOps#getOrUpdateResource]] is that - * this one allows the loading function to fail finding the value, - * i.e. return [[scala.None]]. + * The difference between this method and + * [[com.evolution.scache.Cache.CacheOps#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. @@ -72,8 +74,9 @@ private[scache] object CacheOpsCompat { * The function to run to load the missing value with. * * @return - * The same semantics applies as in [[com.evolution.scache.Cache.CacheOps#getOrUpdateResource]], except that - * the method may return [[scala.None]] in case `value` completes to + * The same semantics applies as in + * [[com.evolution.scache.Cache.CacheOps#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. */ diff --git a/src/main/scala/com/evolution/scache/Cache.scala b/src/main/scala/com/evolution/scache/Cache.scala index e6211c2..5f5da62 100644 --- a/src/main/scala/com/evolution/scache/Cache.scala +++ b/src/main/scala/com/evolution/scache/Cache.scala @@ -4,7 +4,16 @@ import cats.effect.kernel.MonadCancel import cats.effect.{Concurrent, Resource, Temporal} import cats.effect.syntax.all.* import cats.syntax.all.* -import cats.{Applicative, Functor, Hash, Monad, MonadThrow, Monoid, Parallel, ~>} +import cats.{ + Applicative, + Functor, + Hash, + Monad, + MonadThrow, + Monoid, + Parallel, + ~> +} import cats.kernel.CommutativeMonoid import com.evolution.scache.Cache.Directive import com.evolutiongaming.catshelper.CatsHelper.* @@ -13,9 +22,9 @@ import com.evolutiongaming.catshelper.{MeasureDuration, Runtime} /** 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. + * 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]]. @@ -134,7 +143,9 @@ trait Cache[F[_], K, V] { * `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]]] + def getOrUpdate1[A](key: K)( + value: => F[(A, V, Option[Release])] + ): F[Either[A, Either[F[V], V]]] /** Gets a value for specific key, or tries to load it. * @@ -206,8 +217,8 @@ trait Cache[F[_], K, V] { * @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]]. + * 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, @@ -221,27 +232,33 @@ trait Cache[F[_], K, V] { /** Atomically modify a value under specific key. * - * Allows to make a decision regarding value update based on the present value (or its absence), - * and express it as either `Put`, `Ignore`, or `Remove` directive. + * Allows to make a decision regarding value update based on the present + * value (or its absence), and express it as either `Put`, `Ignore`, or + * `Remove` directive. * - * It will try to calculate `f` and apply resulting directive until it succeeds. + * It will try to calculate `f` and apply resulting directive until it + * succeeds. * - * In case of `Put` directive, it is guaranteed that the value is written in cache, - * and that it replaced exactly the value passed to `f`. + * In case of `Put` directive, it is guaranteed that the value is written in + * cache, and that it replaced exactly the value passed to `f`. * * In case of `Remove` directive, it is guaranteed that the key was removed * when it contained exactly the value passed to `f`. * * @param key - * The key to modify value for. + * The key to modify value for. * @param f - * Function that accepts current value found in cache (or None, if it's absent), and returns - * a directive expressing a desired operation on the value, as well as an arbitrary output value of type `A` + * Function that accepts current value found in cache (or None, if it's + * absent), and returns a directive expressing a desired operation on the + * value, as well as an arbitrary output value of type `A` * @return - * Output value returned by `f`, and an optional effect representing an ongoing release of the value - * that was removed from cache as a result of the modification (e.g.: in case of `Put` or `Remove` directives). + * Output value returned by `f`, and an optional effect representing an + * ongoing release of the value that was removed from cache as a result of + * the modification (e.g.: in case of `Put` or `Remove` directives). */ - def modify[A](key: K)(f: Option[V] => (A, Directive[F, V])): F[(A, Option[F[Unit]])] + def modify[A](key: K)( + f: Option[V] => (A, Directive[F, V]) + ): F[(A, Option[F[Unit]])] /** Checks if the value for the key is present in the cache. * @@ -372,10 +389,10 @@ trait Cache[F[_], K, V] { object Cache { import CacheOpsCompat.* - sealed trait Directive[+F[_], +V] object Directive { - final case class Put[F[_], V](value: V, release: Option[F[Unit]]) extends Directive[F, V] + final case class Put[F[_], V](value: V, release: Option[F[Unit]]) + extends Directive[F, V] case object Remove extends Directive[Nothing, Nothing] case object Ignore extends Directive[Nothing, Nothing] } @@ -409,9 +426,12 @@ object Cache { def getOrUpdateOpt(key: K)(value: => F[Option[V]]) = value - def put(key: K, value: V, release: Option[F[Unit]]) = none[V].pure[F].pure[F] + def put(key: K, value: V, release: Option[F[Unit]]) = + none[V].pure[F].pure[F] - def modify[A](key: K)(f: Option[V] => (A, Directive[F, V])): F[(A, Option[F[Unit]])] = + def modify[A](key: K)( + f: Option[V] => (A, Directive[F, V]) + ): F[(A, Option[F[Unit]])] = (f(None)._1, none[F[Unit]]).pure[F] def contains(key: K) = false.pure[F] @@ -428,9 +448,11 @@ object Cache { def clear = ().pure[F].pure[F] - def foldMap[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = Monoid[A].empty.pure[F] + def foldMap[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = + Monoid[A].empty.pure[F] - def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = Monoid[A].empty.pure[F] + def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = + Monoid[A].empty.pure[F] } } @@ -450,7 +472,8 @@ object Cache { * 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]] = { + def loading[F[_]: Concurrent: Parallel: Runtime, K, V] + : Resource[F, Cache[F, K, V]] = { loading(none) } @@ -470,7 +493,9 @@ object Cache { * 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]] = { + def loading[F[_]: Concurrent: Parallel: Runtime, K, V]( + partitions: Int + ): Resource[F, Cache[F, K, V]] = { loading(partitions.some) } @@ -524,7 +549,9 @@ object Cache { * 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]] = { + 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] @@ -533,8 +560,11 @@ object Cache { .map { _.pure[F] } .getOrElse { NrOfPartitions[F]() } .toResource - cache = LoadingCache.of(LoadingCache.EntryRefs.empty[F, K, V]) - partitions <- Partitions.of[Resource[F, _], K, Cache[F, K, V]](nrOfPartitions, _ => cache) + cache = LoadingCache.of(LoadingCache.EntryRefs.empty[F, K, V]) + partitions <- Partitions.of[Resource[F, _], K, Cache[F, K, V]]( + nrOfPartitions, + _ => cache + ) } yield { fromPartitions(partitions) } @@ -589,8 +619,8 @@ object Cache { * also released. */ def expiring[F[_]: Temporal: Runtime: Parallel, K, V]( - config: ExpiringCache.Config[F, K, V], - partitions: Option[Int] = None + config: ExpiringCache.Config[F, K, V], + partitions: Option[Int] = None ): Resource[F, Cache[F, K, V]] = { implicit val hash: Hash[K] = Hash.fromUniversalHashCode[K] @@ -600,15 +630,17 @@ object Cache { .map { _.pure[F] } .getOrElse { NrOfPartitions[F]() } .toResource - config1 = config - .maxSize + config1 = config.maxSize .fold { config - } { - maxSize => config.copy(maxSize = (maxSize * 1.1 / nrOfPartitions).toInt.some) + } { maxSize => + config.copy(maxSize = (maxSize * 1.1 / nrOfPartitions).toInt.some) } - cache = ExpiringCache.of[F, K, V](config1) - partitions <- Partitions.of[Resource[F, _], K, Cache[F, K, V]](nrOfPartitions, _ => cache) + cache = ExpiringCache.of[F, K, V](config1) + partitions <- Partitions.of[Resource[F, _], K, Cache[F, K, V]]( + nrOfPartitions, + _ => cache + ) } yield { fromPartitions(partitions) } @@ -636,18 +668,23 @@ object Cache { * - [[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] = { + def fromPartitions[F[_]: MonadThrow: Parallel, K, V]( + partitions: Partitions[K, Cache[F, K, V]] + ): Cache[F, K, V] = { PartitionedCache(partitions) } - private[scache] abstract class Abstract0[F[_], K, V] extends Cache[F, K, V] { self => + private[scache] abstract class Abstract0[F[_], K, V] extends Cache[F, K, V] { + self => def put(key: K, value: V) = self.put(key, value, none) - def put(key: K, value: V, release: Release) = self.put(key, value, release.some) + def put(key: K, value: V, release: Release) = + self.put(key, value, release.some) } - private[scache] abstract class Abstract1[F[_]: MonadThrow, K, V] extends Abstract0[F, K, V] { self => + private[scache] abstract class Abstract1[F[_]: MonadThrow, K, V] + extends Abstract0[F, K, V] { self => def getOrUpdateOpt(key: K)(value: => F[Option[V]]) = { self @@ -669,18 +706,18 @@ object Cache { private sealed abstract class MapK - implicit class CacheOps[F[_], K, V](val self: Cache[F, K, V]) extends AnyVal { - def withMetrics( - metrics: CacheMetrics[F])(implicit - temporal: Temporal[F], - measureDuration: MeasureDuration[F] + def withMetrics(metrics: CacheMetrics[F])(implicit + temporal: Temporal[F], + measureDuration: MeasureDuration[F] ): Resource[F, Cache[F, K, V]] = { CacheMetered(self, metrics) } - def mapK[G[_]](fg: F ~> G, gf: G ~> F)(implicit F: Functor[F]): Cache[G, K, V] = { + def mapK[G[_]](fg: F ~> G, gf: G ~> F)(implicit + F: Functor[F] + ): Cache[G, K, V] = { new MapK with Cache[G, K, V] { def get(key: K) = fg(self.get(key)) @@ -693,12 +730,18 @@ object Cache { } } - def getOrUpdate(key: K)(value: => G[V]) = fg(self.getOrUpdate(key)(gf(value))) + def getOrUpdate(key: K)(value: => G[V]) = fg( + self.getOrUpdate(key)(gf(value)) + ) def getOrUpdate1[A](key: K)(value: => G[(A, V, Option[Release])]) = { fg { self - .getOrUpdate1(key) { gf(value).map { case (a, value, release) => (a, value, release.map { a => gf(a) }) } } + .getOrUpdate1(key) { + gf(value).map { case (a, value, release) => + (a, value, release.map { a => gf(a) }) + } + } .map { _.map { _.leftMap { a => fg(a) } } } } } @@ -715,16 +758,19 @@ object Cache { } } - def modify[A](key: K)(f: Option[V] => (A, Directive[G, V])): G[(A, Option[G[Unit]])] = { + def modify[A]( + key: K + )(f: Option[V] => (A, Directive[G, V])): G[(A, Option[G[Unit]])] = { val adaptedF: Option[V] => (A, Directive[F, V]) = f(_) match { - case (a, put: Directive.Put[G, V]) => (a, Directive.Put(put.value, put.release.map(gf(_)))) + case (a, put: Directive.Put[G, V]) => + (a, Directive.Put(put.value, put.release.map(gf(_)))) case (a, Directive.Ignore) => (a, Directive.Ignore) case (a, Directive.Remove) => (a, Directive.Remove) } fg { self .modify(key)(adaptedF) - .map { case (a, release) => (a, release.map(fg(_)))} + .map { case (a, release) => (a, release.map(fg(_))) } } } @@ -753,8 +799,7 @@ object Cache { def values = fg(self.values.map(_.map { case (k, v) => (k, fg(v)) })) def values1 = fg { - self - .values1 + self.values1 .map { _.map { case (k, v) => (k, v.leftMap { v => fg(v) }) } } } @@ -766,7 +811,9 @@ object Cache { fg(self.foldMap { case (k, v) => gf(f(k, v.leftMap { v => fg(v) })) }) } - def foldMapPar[A: CommutativeMonoid](f: (K, Either[G[V], V]) => G[A]) = { + def foldMapPar[A: CommutativeMonoid]( + f: (K, Either[G[V], V]) => G[A] + ) = { fg(self.foldMap { case (k, v) => gf(f(k, v.leftMap { v => fg(v) })) }) } } @@ -777,7 +824,8 @@ object Cache { * 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) + 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. * @@ -832,10 +880,8 @@ object Cache { * in this method `F[_]` will only complete when the value is fully * loaded. */ - def getOrUpdate2[A]( - key: K)( - value: => F[(A, V, Option[F[Unit]])])(implicit - F: Monad[F] + def getOrUpdate2[A](key: K)(value: => F[(A, V, Option[F[Unit]])])(implicit + F: Monad[F] ): F[Either[A, V]] = { self .getOrUpdate1(key) { value } @@ -852,7 +898,6 @@ object Cache { * 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 @@ -873,9 +918,10 @@ object Cache { * the method may return [[scala.None]] in case `value` completes to * [[scala.None]]. */ - def getOrUpdateOpt1[A](key: K)( - value: => F[Option[(A, V, Option[F[Unit]])]])(implicit - F: MonadThrow[F] + def getOrUpdateOpt1[A]( + key: K + )(value: => F[Option[(A, V, Option[F[Unit]])]])(implicit + F: MonadThrow[F] ): F[Option[Either[A, Either[F[V], V]]]] = { self.getOrUpdateOpt1Compat(key)(value) } @@ -905,11 +951,12 @@ object Cache { * [[#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] = { + def getOrUpdateResource( + key: K + )(value: => Resource[F, V])(implicit F: MonadCancel[F, Throwable]): F[V] = { self .getOrUpdate1(key) { - value - .allocated + value.allocated .map { case (a, release) => (a, a, release.some) } } .flatMap { @@ -921,9 +968,9 @@ object 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]]. + * 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. @@ -936,61 +983,80 @@ object Cache { * [[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]] = { + def getOrUpdateResourceOpt(key: K)( + value: => Resource[F, Option[V]] + )(implicit F: MonadCancel[F, Throwable]): F[Option[V]] = { self.getOrUpdateResourceOptCompat(key) { value } } /** Like `modify`, but doesn't pass through any return value. - * - * @return - * If this `update` replaced an existing value, - * will return `Some` containing an effect representing release of that value. - */ - def update(key: K)(f: Option[V] => Directive[F, V])(implicit F: Functor[F]): F[Option[F[Unit]]] = + * + * @return + * If this `update` replaced an existing value, will return `Some` + * containing an effect representing release of that value. + */ + def update(key: K)(f: Option[V] => Directive[F, V])(implicit + F: Functor[F] + ): F[Option[F[Unit]]] = self.modify(key)(() -> f(_)).map(_._2) - /** Like `modify`, but `f` is only applied if there is a value present in cache, - * and the result is always replacing the old value. - * - * @return - * `true` if value was present, and was subsequently replaced. - * `false` if there was no value present. - */ + /** Like `modify`, but `f` is only applied if there is a value present in + * cache, and the result is always replacing the old value. + * + * @return + * `true` if value was present, and was subsequently replaced. `false` if + * there was no value present. + */ def updatePresent(key: K)(f: V => V)(implicit F: Functor[F]): F[Boolean] = self.modify[Boolean](key) { case Some(value) => (true, Directive.Put(f(value), None)) - case None => (false, Directive.Ignore) - } map(_._1) - - /** Like `update`, but `f` has an option to return `None`, in which case value will not be changed. - * - * @return - * `true` if value was present and was subsequently replaced. - * `false` if there was no value present, or it was not replaced. - */ - def updatePresentOpt(key: K)(f: V => Option[V])(implicit F: Functor[F]): F[Boolean] = + case None => (false, Directive.Ignore) + } map (_._1) + + /** Like `update`, but `f` has an option to return `None`, in which case + * value will not be changed. + * + * @return + * `true` if value was present and was subsequently replaced. `false` if + * there was no value present, or it was not replaced. + */ + def updatePresentOpt( + key: K + )(f: V => Option[V])(implicit F: Functor[F]): F[Boolean] = self.modify[Boolean](key) { - case Some(value) => f(value).fold[(Boolean, Directive[F, V])](false -> Directive.Ignore)(v => true -> Directive.Put(v, None)) + case Some(value) => + f(value).fold[(Boolean, Directive[F, V])](false -> Directive.Ignore)( + v => true -> Directive.Put(v, None) + ) case None => (false, Directive.Ignore) - } map(_._1) - - /** Like `put`, but based on `modify`, and guarantees that as a result of the operation the value was in fact - * written in cache. Will be slower than a regular `put` in situations of high contention. - * - * @return - * If this `putStrict` replaced an existing value, will return `Some` containing the old value - * and an effect representing release of that value. - */ - def putStrict(key: K, value: V)(implicit F: Applicative[F]): F[Option[(V, F[Unit])]] = + } map (_._1) + + /** Like `put`, but based on `modify`, and guarantees that as a result of + * the operation the value was in fact written in cache. Will be slower + * than a regular `put` in situations of high contention. + * + * @return + * If this `putStrict` replaced an existing value, will return `Some` + * containing the old value and an effect representing release of that + * value. + */ + def putStrict(key: K, value: V)(implicit + F: Applicative[F] + ): F[Option[(V, F[Unit])]] = self.modify[Option[V]](key)((_, Directive.Put(value, None))).map(_.tupled) /** Like `putStrict`, but with `release` part of the new value. - * - * @return - * If this `putStrict` replaced an existing value, will return `Some` containing the old value - * and an effect representing release of that value. - */ - def putStrict(key: K, value: V, release: self.type#Release)(implicit F: Applicative[F]): F[Option[(V, F[Unit])]] = - self.modify[Option[V]](key)((_, Directive.Put(value, release.some))).map(_.tupled) + * + * @return + * If this `putStrict` replaced an existing value, will return `Some` + * containing the old value and an effect representing release of that + * value. + */ + def putStrict(key: K, value: V, release: self.type#Release)(implicit + F: Applicative[F] + ): F[Option[(V, F[Unit])]] = + self + .modify[Option[V]](key)((_, Directive.Put(value, release.some))) + .map(_.tupled) } } diff --git a/src/main/scala/com/evolution/scache/CacheFenced.scala b/src/main/scala/com/evolution/scache/CacheFenced.scala index 6c9259a..74edac2 100644 --- a/src/main/scala/com/evolution/scache/CacheFenced.scala +++ b/src/main/scala/com/evolution/scache/CacheFenced.scala @@ -6,12 +6,13 @@ import cats.kernel.CommutativeMonoid import cats.syntax.all.* import com.evolutiongaming.catshelper.CatsHelper.* -/** - * Prevents adding new resources to cache after it was released +/** Prevents adding new resources to cache after it was released */ object CacheFenced { - def of[F[_]: Concurrent, K, V](cache: Cache[F, K, V]): Resource[F, Cache[F, K, V]] = { + def of[F[_]: Concurrent, K, V]( + cache: Cache[F, K, V] + ): Resource[F, Cache[F, K, V]] = { Resource .make { Ref[F].of(().pure[F]) @@ -24,7 +25,10 @@ object CacheFenced { .fenced } - def apply[F[_]: MonadThrow, K, V](cache: Cache[F, K, V], fence: F[Unit]): Cache[F, K, V] = { + def apply[F[_]: MonadThrow, K, V]( + cache: Cache[F, K, V], + fence: F[Unit] + ): Cache[F, K, V] = { abstract class CacheFenced extends Cache.Abstract1[F, K, V] new CacheFenced { @@ -52,7 +56,9 @@ object CacheFenced { .productR { cache.put(key, value, release) } } - def modify[A](key: K)(f: Option[V] => (A, Cache.Directive[F, V])): F[(A, Option[F[Unit]])] = + def modify[A](key: K)( + f: Option[V] => (A, Cache.Directive[F, V]) + ): F[(A, Option[F[Unit]])] = fence.flatMap(_ => cache.modify(key)(f)) def contains(key: K) = cache.contains(key) @@ -69,9 +75,11 @@ object CacheFenced { def clear = cache.clear - def foldMap[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = cache.foldMap(f) + def foldMap[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = + cache.foldMap(f) - def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = cache.foldMapPar(f) + def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = + cache.foldMapPar(f) } } diff --git a/src/main/scala/com/evolution/scache/CacheMetered.scala b/src/main/scala/com/evolution/scache/CacheMetered.scala index 3753228..7b32ba3 100644 --- a/src/main/scala/com/evolution/scache/CacheMetered.scala +++ b/src/main/scala/com/evolution/scache/CacheMetered.scala @@ -11,15 +11,15 @@ import scala.concurrent.duration.* object CacheMetered { def apply[F[_]: MeasureDuration: Temporal, K, V]( - cache: Cache[F, K, V], - metrics: CacheMetrics[F], - interval: FiniteDuration = 1.minute + cache: Cache[F, K, V], + metrics: CacheMetrics[F], + interval: FiniteDuration = 1.minute ): Resource[F, Cache[F, K, V]] = { def measureSize = { for { size <- cache.size - _ <- metrics.size(size) + _ <- metrics.size(size) } yield {} } @@ -64,20 +64,25 @@ object CacheMetered { for { result <- cache.getOrUpdate1(key) { for { - _ <- metrics.get(false) - start <- MeasureDuration[F].start - value <- value.attempt - duration <- start + _ <- metrics.get(false) + start <- MeasureDuration[F].start + value <- value.attempt + duration <- start loadSucceed = value match { case Right(_) | Left(CacheOpsCompat.NoneError) => true - case Left(_) => false + case Left(_) => false } - _ <- metrics.load(duration, loadSucceed) + _ <- metrics.load(duration, loadSucceed) value <- value.liftTo[F] } yield { val (a, v, release) = value - val release1 = releaseMetered(start, release.getOrElse { ().pure[F] }) - (a, v, release1.some) // TODO is this a good idea to convert option to always some? + val release1 = + releaseMetered(start, release.getOrElse { ().pure[F] }) + ( + a, + v, + release1.some + ) // TODO is this a good idea to convert option to always some? } } _ <- metrics.get(true).whenA(result.isRight) @@ -87,30 +92,49 @@ object CacheMetered { def put(key: K, value: V, release: Option[Release]) = { for { duration <- MeasureDuration[F].start - _ <- metrics.put - release1 = releaseMetered(duration, release.getOrElse { ().pure[F] }) - value <- cache.put(key, value, release1.some) + _ <- metrics.put + release1 = releaseMetered( + duration, + release.getOrElse { ().pure[F] } + ) + value <- cache.put(key, value, release1.some) } yield value } - def modify[A](key: K)(f: Option[V] => (A, Directive[F, V])): F[(A, Option[F[Unit]])] = + def modify[A]( + key: K + )(f: Option[V] => (A, Directive[F, V])): F[(A, Option[F[Unit]])] = for { duration <- MeasureDuration[F].start - ((a, entryExisted, directive), release) <- cache.modify(key) { entry => - f(entry) match { - case (a, put: Directive.Put[F, V]) => - ((a, entry.nonEmpty, CacheMetrics.Directive.Put), - Directive.Put(put.value, releaseMetered(duration, put.release.getOrElse(().pure[F])).some)) - case (a, Directive.Ignore) => - ((a, entry.nonEmpty, CacheMetrics.Directive.Ignore), Directive.Ignore) - case (a, Directive.Remove) => - ((a, entry.nonEmpty, CacheMetrics.Directive.Remove), Directive.Remove) - } + ((a, entryExisted, directive), release) <- cache.modify(key) { + entry => + f(entry) match { + case (a, put: Directive.Put[F, V]) => + ( + (a, entry.nonEmpty, CacheMetrics.Directive.Put), + Directive.Put( + put.value, + releaseMetered( + duration, + put.release.getOrElse(().pure[F]) + ).some + ) + ) + case (a, Directive.Ignore) => + ( + (a, entry.nonEmpty, CacheMetrics.Directive.Ignore), + Directive.Ignore + ) + case (a, Directive.Remove) => + ( + (a, entry.nonEmpty, CacheMetrics.Directive.Remove), + Directive.Remove + ) + } } _ <- metrics.modify(entryExisted, directive) } yield (a, release) - def contains(key: K) = cache.contains(key) def size = { @@ -169,7 +193,9 @@ object CacheMetered { } yield a } - def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = { + def foldMapPar[A: CommutativeMonoid]( + f: (K, Either[F[V], V]) => F[A] + ) = { for { d <- MeasureDuration[F].start a <- cache.foldMapPar(f) @@ -180,4 +206,4 @@ object CacheMetered { } } } -} \ No newline at end of file +} diff --git a/src/main/scala/com/evolution/scache/CacheMetrics.scala b/src/main/scala/com/evolution/scache/CacheMetrics.scala index 159dc82..010ccb7 100644 --- a/src/main/scala/com/evolution/scache/CacheMetrics.scala +++ b/src/main/scala/com/evolution/scache/CacheMetrics.scala @@ -5,7 +5,12 @@ import cats.effect.Resource import cats.syntax.all.* import com.evolution.scache.CacheMetrics.Directive import com.evolutiongaming.smetrics.MetricsHelper.* -import com.evolutiongaming.smetrics.{CollectorRegistry, LabelNames, Quantile, Quantiles} +import com.evolutiongaming.smetrics.{ + CollectorRegistry, + LabelNames, + Quantile, + Quantiles +} import scala.concurrent.duration.FiniteDuration @@ -36,8 +41,7 @@ trait CacheMetrics[F[_]] { object CacheMetrics { - def empty[F[_] : Applicative]: CacheMetrics[F] = const(().pure[F]) - + def empty[F[_]: Applicative]: CacheMetrics[F] = const(().pure[F]) def const[F[_]](unit: F[Unit]): CacheMetrics[F] = new CacheMetrics[F] { @@ -66,7 +70,7 @@ object CacheMetrics { sealed trait Directive { override def toString: Prefix = this match { - case Directive.Put => "put" + case Directive.Put => "put" case Directive.Ignore => "ignore" case Directive.Remove => "remove" } @@ -85,148 +89,156 @@ object CacheMetrics { val Default: Prefix = "cache" } - - def of[F[_] : Monad]( - collectorRegistry: CollectorRegistry[F], - prefix: Prefix = Prefix.Default, + def of[F[_]: Monad]( + collectorRegistry: CollectorRegistry[F], + prefix: Prefix = Prefix.Default ): Resource[F, Name => CacheMetrics[F]] = { val getCounter = collectorRegistry.counter( - name = s"${ prefix }_get", + name = s"${prefix}_get", help = "Get type: hit or miss", - labels = LabelNames("name", "type")) + labels = LabelNames("name", "type") + ) val putCounter = collectorRegistry.counter( - name = s"${ prefix }_put", + name = s"${prefix}_put", help = "Put", - labels = LabelNames("name")) + labels = LabelNames("name") + ) val modifyCounter = collectorRegistry.counter( - name = s"${ prefix }_modify", - help = "Modify, labeled by modification input (entry was present or not), and output (put, keep, or remove)", + name = s"${prefix}_modify", + help = + "Modify, labeled by modification input (entry was present or not), and output (put, keep, or remove)", labels = LabelNames("name", "existing_entry", "result") ) val loadResultCounter = collectorRegistry.counter( - name = s"${ prefix }_load_result", + name = s"${prefix}_load_result", help = "Load result: success or failure", - labels = LabelNames("name", "result")) + labels = LabelNames("name", "result") + ) val quantiles = Quantiles( Quantile(value = 0.9, error = 0.05), - Quantile(value = 0.99, error = 0.005)) + Quantile(value = 0.99, error = 0.005) + ) val loadTimeSummary = collectorRegistry.summary( - name = s"${ prefix }_load_time", + name = s"${prefix}_load_time", help = s"Load time in seconds", quantiles = quantiles, - labels = LabelNames("name", "result")) + labels = LabelNames("name", "result") + ) val sizeGauge = collectorRegistry.gauge( - name = s"${ prefix }_size", + name = s"${prefix}_size", help = s"Cache size", - labels = LabelNames("name")) + labels = LabelNames("name") + ) val lifeTimeSummary = collectorRegistry.summary( - name = s"${ prefix }_life_time", + name = s"${prefix}_life_time", help = s"Life time in seconds", quantiles = quantiles, - labels = LabelNames("name")) + labels = LabelNames("name") + ) val callSummary = collectorRegistry.summary( - name = s"${ prefix }_call_latency", + name = s"${prefix}_call_latency", help = "Call latency in seconds", quantiles = quantiles, - labels = LabelNames("name", "type")) + labels = LabelNames("name", "type") + ) for { - getsCounter <- getCounter - putCounter <- putCounter - modifyCounter <- modifyCounter + getsCounter <- getCounter + putCounter <- putCounter + modifyCounter <- modifyCounter loadResultCounter <- loadResultCounter - loadTimeSummary <- loadTimeSummary - lifeTimeSummary <- lifeTimeSummary - sizeGauge <- sizeGauge - callSummary <- callSummary - } yield { - (name: Name) => + loadTimeSummary <- loadTimeSummary + lifeTimeSummary <- lifeTimeSummary + sizeGauge <- sizeGauge + callSummary <- callSummary + } yield { (name: Name) => + val hitCounter = getsCounter.labels(name, "hit") - val hitCounter = getsCounter.labels(name, "hit") + val missCounter = getsCounter.labels(name, "miss") - val missCounter = getsCounter.labels(name, "miss") + val successCounter = loadResultCounter.labels(name, "success") - val successCounter = loadResultCounter.labels(name, "success") + val failureCounter = loadResultCounter.labels(name, "failure") - val failureCounter = loadResultCounter.labels(name, "failure") + val successSummary = loadTimeSummary.labels(name, "success") - val successSummary = loadTimeSummary.labels(name, "success") + val failureSummary = loadTimeSummary.labels(name, "failure") - val failureSummary = loadTimeSummary.labels(name, "failure") + val putCounter1 = putCounter.labels(name) - val putCounter1 = putCounter.labels(name) + val lifeTimeSummary1 = lifeTimeSummary.labels(name) - val lifeTimeSummary1 = lifeTimeSummary.labels(name) + val sizeSummary = callSummary.labels(name, "size") - val sizeSummary = callSummary.labels(name, "size") + val keysSummary = callSummary.labels(name, "keys") - val keysSummary = callSummary.labels(name, "keys") + val valuesSummary = callSummary.labels(name, "values") - val valuesSummary = callSummary.labels(name, "values") + val clearSummary = callSummary.labels(name, "clear") - val clearSummary = callSummary.labels(name, "clear") + val foldMapSummary = callSummary.labels(name, "foldMap") - val foldMapSummary = callSummary.labels(name, "foldMap") + new CacheMetrics[F] { - new CacheMetrics[F] { - - def get(hit: Boolean) = { - val counter = if (hit) hitCounter else missCounter - counter.inc() - } + def get(hit: Boolean) = { + val counter = if (hit) hitCounter else missCounter + counter.inc() + } - def load(time: FiniteDuration, success: Boolean) = { - val resultCounter = if (success) successCounter else failureCounter - val timeSummary = if (success) successSummary else failureSummary - for { - _ <- resultCounter.inc() - _ <- timeSummary.observe(time.toNanos.nanosToSeconds) - } yield {} - } + def load(time: FiniteDuration, success: Boolean) = { + val resultCounter = if (success) successCounter else failureCounter + val timeSummary = if (success) successSummary else failureSummary + for { + _ <- resultCounter.inc() + _ <- timeSummary.observe(time.toNanos.nanosToSeconds) + } yield {} + } - def life(time: FiniteDuration) = { - lifeTimeSummary1.observe(time.toNanos.nanosToSeconds) - } + def life(time: FiniteDuration) = { + lifeTimeSummary1.observe(time.toNanos.nanosToSeconds) + } - val put = putCounter1.inc() + val put = putCounter1.inc() - def modify(entryExisted: Boolean, directive: Directive): F[Unit] = { - modifyCounter.labels(name, entryExisted.toString, directive.toString).inc() - } + def modify(entryExisted: Boolean, directive: Directive): F[Unit] = { + modifyCounter + .labels(name, entryExisted.toString, directive.toString) + .inc() + } - def size(size: Int) = { - sizeGauge.labels(name).set(size.toDouble) - } + def size(size: Int) = { + sizeGauge.labels(name).set(size.toDouble) + } - def size(latency: FiniteDuration) = { - sizeSummary.observe(latency.toNanos.nanosToSeconds) - } + def size(latency: FiniteDuration) = { + sizeSummary.observe(latency.toNanos.nanosToSeconds) + } - def values(latency: FiniteDuration) = { - valuesSummary.observe(latency.toNanos.nanosToSeconds) - } + def values(latency: FiniteDuration) = { + valuesSummary.observe(latency.toNanos.nanosToSeconds) + } - def keys(latency: FiniteDuration) = { - keysSummary.observe(latency.toNanos.nanosToSeconds) - } + def keys(latency: FiniteDuration) = { + keysSummary.observe(latency.toNanos.nanosToSeconds) + } - def clear(latency: FiniteDuration) = { - clearSummary.observe(latency.toNanos.nanosToSeconds) - } + def clear(latency: FiniteDuration) = { + clearSummary.observe(latency.toNanos.nanosToSeconds) + } - def foldMap(latency: FiniteDuration) = { - foldMapSummary.observe(latency.toNanos.nanosToSeconds) - } + def foldMap(latency: FiniteDuration) = { + foldMapSummary.observe(latency.toNanos.nanosToSeconds) } + } } } -} \ 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 98da05a..7dd5d32 100644 --- a/src/main/scala/com/evolution/scache/ExpiringCache.scala +++ b/src/main/scala/com/evolution/scache/ExpiringCache.scala @@ -17,20 +17,25 @@ object ExpiringCache { type Timestamp = Long private[scache] def of[F[_], K, V]( - config: Config[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 - val expireAfterReadMs = config.expireAfterRead.toMillis + cooldown / 2 + val cooldown = config.expireAfterRead.toMillis / 5 + val expireAfterReadMs = config.expireAfterRead.toMillis + cooldown / 2 val expireAfterWriteMs = config.expireAfterWrite.map { _.toMillis } - val expireInterval = { - val expireInterval = expireAfterWriteMs.fold(expireAfterReadMs) { _ min expireAfterReadMs } + val expireInterval = { + val expireInterval = expireAfterWriteMs.fold(expireAfterReadMs) { + _ min expireAfterReadMs + } (expireInterval / 10).millis } - def removeExpiredAndCheckSize(ref: Ref[F, LoadingCache.EntryRefs[F, K, E]], cache: Cache[F, K, E]) = { + def removeExpiredAndCheckSize( + ref: Ref[F, LoadingCache.EntryRefs[F, K, E]], + cache: Cache[F, K, E] + ) = { def remove(key: K) = { cache @@ -39,20 +44,26 @@ object ExpiringCache { .void } - def removeExpired(key: K, entryRef: LoadingCache.EntryRef[F, Entry[V]]) = { - entryRef - .get + def removeExpired( + key: K, + entryRef: LoadingCache.EntryRef[F, Entry[V]] + ) = { + entryRef.get .flatMap { case state: EntryState.Value[F, Entry[V]] => for { - now <- Clock[F].millis - expiredAfterRead = expireAfterReadMs + state.entry.value.touched < now - expiredAfterWrite = () => expireAfterWriteMs.exists { _ + state.entry.value.created < now } - expired = expiredAfterRead || expiredAfterWrite() - result <- if (expired) remove(key) else ().pure[F] + now <- Clock[F].millis + expiredAfterRead = + expireAfterReadMs + state.entry.value.touched < now + expiredAfterWrite = () => + expireAfterWriteMs.exists { + _ + state.entry.value.created < now + } + expired = expiredAfterRead || expiredAfterWrite() + result <- if (expired) remove(key) else ().pure[F] } yield result case _: EntryState.Loading[F, Entry[V]] => ().pure[F] - case EntryState.Removed => ().pure[F] + case EntryState.Removed => ().pure[F] } } @@ -66,12 +77,12 @@ object ExpiringCache { entryRefs .foldLeft(zero.pure[F]) { case (result, (key, entryRef)) => result.flatMap { result => - entryRef - .get + entryRef.get .map { - case state: EntryState.Value[F, Entry[V]] => Elem(key, state.entry.value.touched) :: result + case state: EntryState.Value[F, Entry[V]] => + Elem(key, state.entry.value.touched) :: result case _: EntryState.Loading[F, Entry[V]] => result - case EntryState.Removed => result + case EntryState.Removed => result } } } @@ -85,55 +96,56 @@ object ExpiringCache { for { entryRefs <- ref.get - result <- if (entryRefs.size > maxSize) drop(entryRefs) else ().pure[F] + result <- + if (entryRefs.size > maxSize) drop(entryRefs) else ().pure[F] } yield result } for { entryRefs <- ref.get - result <- entryRefs.foldMapM { case (key, entryRef) => removeExpired(key, entryRef) } - _ <- config - .maxSize + result <- entryRefs.foldMapM { case (key, entryRef) => + removeExpired(key, entryRef) + } + _ <- config.maxSize .foldMapM { maxSize => notExceedMaxSize(maxSize) } } yield result } def refreshEntries( - refresh: Refresh[K, F[Option[V]]], - ref: Ref[F, LoadingCache.EntryRefs[F, K, E]], - cache: Cache[F, K, E] + refresh: Refresh[K, F[Option[V]]], + ref: Ref[F, LoadingCache.EntryRefs[F, K, E]], + cache: Cache[F, K, E] ) = { - ref - .get + ref.get .flatMap { entryRefs => entryRefs.foldMapM { case (key, entryRef) => - entryRef - .get + entryRef.get .flatMap { case _: EntryState.Value[F, Entry[V]] => refresh .value(key) .flatMap { - case Some(value) => entryRef.update1 { _.copy(value = value) } - case None => cache.remove(key).void + case Some(value) => + entryRef.update1 { _.copy(value = value) } + case None => cache.remove(key).void } .handleError { _ => () } case _: EntryState.Loading[F, Entry[V]] => ().pure[F] - case EntryState.Removed => ().pure[F] + case EntryState.Removed => ().pure[F] } } } } - def schedule(interval: FiniteDuration)(fa: F[Unit]) = Schedule(interval, interval)(fa) + def schedule(interval: FiniteDuration)(fa: F[Unit]) = + Schedule(interval, interval)(fa) val entryRefs = LoadingCache.EntryRefs.empty[F, K, E] for { - ref <- Ref[F].of(entryRefs).toResource + ref <- Ref[F].of(entryRefs).toResource cache <- LoadingCache.of(ref) - _ <- schedule(expireInterval) { removeExpiredAndCheckSize(ref, cache) } - _ <- config - .refresh + _ <- schedule(expireInterval) { removeExpiredAndCheckSize(ref, cache) } + _ <- config.refresh .foldMapM { refresh => schedule(refresh.interval) { refreshEntries(refresh, ref, cache) } } @@ -142,18 +154,16 @@ object ExpiringCache { } } - - def apply[F[_] : MonadThrow : Clock, K, V]( - ref: Ref[F, LoadingCache.EntryRefs[F, K, Entry[V]]], - cache: Cache[F, K, Entry[V]], - cooldown: Long, + def apply[F[_]: MonadThrow: Clock, K, V]( + ref: Ref[F, LoadingCache.EntryRefs[F, K, Entry[V]]], + cache: Cache[F, K, Entry[V]], + cooldown: Long ): Cache[F, K, V] = { type E = Entry[V] def entryOf(value: V) = { - Clock[F] - .millis + Clock[F].millis .map { timestamp => Entry(value, created = timestamp, read = none) } @@ -163,40 +173,37 @@ object ExpiringCache { def touch(key: K, entry: E) = { for { - now <- Clock[F].millis - result <- if ((entry.touched + cooldown) <= now) { - ref - .get - .flatMap { entries => - entries - .get(key) - .foldMap { _.update1 { _.touch(now) } } - } - } else { - ().pure[F] - } + now <- Clock[F].millis + result <- + if ((entry.touched + cooldown) <= now) { + ref.get + .flatMap { entries => + entries + .get(key) + .foldMap { _.update1 { _.touch(now) } } + } + } else { + ().pure[F] + } } yield result } abstract class ExpiringCache extends Cache.Abstract1[F, K, V] new ExpiringCache { self => - def get(key: K) = { cache .get1(key) .flatMap { case Some(Right(entry)) => touch(key, entry).as { - entry - .value - .some + entry.value.some } - case Some(Left(entry)) => + case Some(Left(entry)) => entry .map { _.value.some } .handleError { _ => none[V] } - case None => + case None => none[V].pure[F] } } @@ -207,18 +214,17 @@ object ExpiringCache { .flatMap { case Some(Right(entry)) => touch(key, entry).as { - entry - .value + entry.value .asRight[F[V]] .some } - case Some(Left(entry)) => + case Some(Left(entry)) => entry .map { _.value } .asLeft[V] .some .pure[F] - case None => + case None => none[Either[F[V], V]].pure[F] } } @@ -242,12 +248,11 @@ object ExpiringCache { .flatMap { case Right(Right(entry)) => touch(key, entry).as { - entry - .value + entry.value .asRight[F[V]] .asRight[A] } - case Right(Left(entry)) => + case Right(Left(entry)) => entry .map { _.value } .asLeft[V] @@ -271,16 +276,25 @@ object ExpiringCache { } // Modifying existing entry creates a new one, since the old one will be released. - def modify[A](key: K)(f: Option[V] => (A, Directive[F, V])): F[(A, Option[F[Unit]])] = - Clock[F] - .millis + def modify[A]( + key: K + )(f: Option[V] => (A, Directive[F, V])): F[(A, Option[F[Unit]])] = + Clock[F].millis .flatMap { timestamp => - val adaptedF: Option[Entry[V]] => (A, Directive[F, Entry[V]]) = entry => f(entry.map(_.value)) match { - case (a, put: Directive.Put[F, V]) => - (a, Directive.Put(Entry(put.value, timestamp, none), put.release)) - case (a, Directive.Ignore) => (a, Directive.Ignore) - case (a, Directive.Remove) => (a, Directive.Remove) - } + val adaptedF: Option[Entry[V]] => (A, Directive[F, Entry[V]]) = + entry => + f(entry.map(_.value)) match { + case (a, put: Directive.Put[F, V]) => + ( + a, + Directive.Put( + Entry(put.value, timestamp, none), + put.release + ) + ) + case (a, Directive.Ignore) => (a, Directive.Ignore) + case (a, Directive.Remove) => (a, Directive.Remove) + } cache.modify(key)(adaptedF) } @@ -291,8 +305,7 @@ object ExpiringCache { def keys = cache.keys def values = { - cache - .values + cache.values .map { values => values.map { case (key, entry) => (key, entry.map { _.value }) @@ -301,8 +314,7 @@ object ExpiringCache { } def values1 = { - cache - .values1 + cache.values1 .map { entries => entries.map { case (key, entry) => val value = entry match { @@ -338,8 +350,11 @@ object ExpiringCache { } } - - final case class Entry[A](value: A, created: Timestamp, read: Option[Timestamp]) { self => + final case class Entry[A]( + value: A, + created: Timestamp, + read: Option[Timestamp] + ) { self => def touch(timestamp: Timestamp): Entry[A] = { if (self.read.forall { timestamp > _ }) copy(read = timestamp.some) @@ -349,7 +364,6 @@ object ExpiringCache { def touched: Timestamp = read.getOrElse(created) } - /** Configuration of a refresh background job. * * Usage example (`SettingService.get` returns `F[Option[Setting]]`): @@ -366,24 +380,24 @@ object ExpiringCache { * 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). + * 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 { def apply[K](interval: FiniteDuration): Apply[K] = new Apply(interval) - private[Refresh] final class Apply[K](val interval: FiniteDuration) extends AnyVal { + private[Refresh] final class Apply[K](val interval: FiniteDuration) + extends AnyVal { def apply[V](f: K => V): Refresh[K, V] = Refresh(interval, f) } } - /** Configuration of expiring cache, including the potential refresh routine. * * Performance consideration: The frequency of internal expiration routine @@ -419,21 +433,21 @@ object ExpiringCache { * 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. + * 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 + * 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, - maxSize: Option[Int] = None, - refresh: Option[Refresh[K, F[Option[V]]]] = None) - + expireAfterRead: FiniteDuration, + expireAfterWrite: Option[FiniteDuration] = None, + maxSize: Option[Int] = None, + refresh: Option[Refresh[K, F[Option[V]]]] = None + ) private implicit class MapOps[K, V](val self: Map[K, V]) extends AnyVal { def foldMapM[F[_]: Monad, A: Monoid](f: (K, V) => F[A]): F[A] = { diff --git a/src/main/scala/com/evolution/scache/LoadingCache.scala b/src/main/scala/com/evolution/scache/LoadingCache.scala index 4e3f119..13a6510 100644 --- a/src/main/scala/com/evolution/scache/LoadingCache.scala +++ b/src/main/scala/com/evolution/scache/LoadingCache.scala @@ -2,7 +2,15 @@ package com.evolution.scache import cats.{Applicative, Functor, Monad, MonadThrow, Parallel} import cats.effect.implicits.* -import cats.effect.{Concurrent, Deferred, Fiber, GenConcurrent, Outcome, Ref, Resource} +import cats.effect.{ + Concurrent, + Deferred, + Fiber, + GenConcurrent, + Outcome, + Ref, + Resource +} import cats.kernel.CommutativeMonoid import com.evolutiongaming.catshelper.ParallelHelper.* import cats.syntax.all.* @@ -10,18 +18,17 @@ import com.evolution.scache.Cache.Directive private[scache] object LoadingCache { - def of[F[_] : Concurrent, K, V]( - map: EntryRefs[F, K, V], + def of[F[_]: Concurrent, K, V]( + map: EntryRefs[F, K, V] ): Resource[F, Cache[F, K, V]] = { for { - ref <- Ref[F].of(map).toResource + ref <- Ref[F].of(map).toResource cache <- of(ref) } yield cache } - - def of[F[_] : Concurrent, K, V]( - ref: Ref[F, EntryRefs[F, K, V]], + def of[F[_]: Concurrent, K, V]( + ref: Ref[F, EntryRefs[F, K, V]] ): Resource[F, Cache[F, K, V]] = { Resource.make { apply(ref).pure[F] @@ -31,15 +38,13 @@ private[scache] object LoadingCache { } def apply[F[_]: Concurrent, K, V]( - ref: Ref[F, EntryRefs[F, K, V]] + ref: Ref[F, EntryRefs[F, K, V]] ): Cache[F, K, V] = { val ignore = (_: Throwable) => () def entryOf(value: V, release: Option[F[Unit]]) = { - Entry( - value = value, - release = release.map { _.handleError(ignore) }) + Entry(value = value, release = release.map { _.handleError(ignore) }) } abstract class LoadingCache extends Cache.Abstract1[F, K, V] @@ -47,30 +52,22 @@ private[scache] object LoadingCache { new LoadingCache { def get(key: K) = { - ref - .get + ref.get .flatMap { entryRefs => entryRefs .get(key) .fold { none[V].pure[F] } { entry => - entry - .get + entry.get .flatMap { - case state: EntryState.Value[F, V] => - state - .entry - .value - .some + case state: EntryState.Value[F, V] => + state.entry.value.some .pure[F] case state: EntryState.Loading[F, V] => - state - .deferred - .get + state.deferred.get .map { entry => - entry - .toOption + entry.toOption .map { _.value } } case EntryState.Removed => @@ -81,8 +78,7 @@ private[scache] object LoadingCache { } def get1(key: K) = { - ref - .get + ref.get .flatMap { entryRefs => entryRefs .get(key) @@ -98,260 +94,288 @@ private[scache] object LoadingCache { } } - def getOrUpdate1[A](key: K)(value: => F[(A, V, Option[Release])]): F[Either[A, Either[F[V], V]]] = { + def getOrUpdate1[A]( + key: K + )(value: => F[(A, V, Option[Release])]): F[Either[A, Either[F[V], V]]] = { 0.tailRecM { counter => - ref - .access + ref.access .flatMap { case (entryRefs, set) => entryRefs .get(key) .fold { for { deferred <- Deferred[F, Either[Throwable, Entry[F, V]]] - entryRef <- Ref[F].of[EntryState[F, V]](EntryState.Loading(deferred)) - result <- set(entryRefs.updated(key, entryRef)) - .flatMap { - case true => - value - .map { case (a, value, release) => - val entry = entryOf(value, release) - (a, entry) - } - .attempt - .race1 { deferred.get } - .flatMap { - // `value` got computed, and deferred was not (yet) completed by any other fiber in `put` - case Left(Right((a, entry))) => - deferred - .complete(entry.asRight) - .flatMap { - // Successfully completed our deferred, - // now trying to place the new value in the entry. - case true => - - def releaseAndReturnValue(state: EntryState.Value[F, V]): F[Either[A, Either[F[V], V]]] = - entry - .release1 - .start - .as { - state - .entry - .value - .asRight[F[V]] - .asRight[A] - } - - def releaseAndReturnLoading(state: EntryState.Loading[F, V]): F[Either[A, Either[F[V], V]]] = - entry - .release1 - .start - .as { - state - .deferred - .getOrError - .map(_.value) - .asLeft[V] - .asRight[A] - } - - // Try putting computed value in the map, if there is no entry with our key. - // If the map already contains an entry with our key, - // return its value (or value computation). - def tryPutNewValue: F[Either[A, Either[F[V], V]]] = - 0.tailRecM { counter => - ref - .access - .flatMap { case (entryRefs, set) => - entryRefs - .get(key) - .fold { - // No entry present in the map, so we try to add a new one - Ref[F] - .of[EntryState[F, V]](EntryState.Value(entry)) - .flatMap { entryRef => - set(entryRefs.updated(key, entryRef)).map { - case true => - a - .asLeft[Either[F[V], V]] - .asRight[Int] - case false => - (counter + 1) - .asLeft[Either[A, Either[F[V], V]]] - } - } - } { entryRef => - entryRef - .get - .flatMap { - case state: EntryState.Value[F, V] => - releaseAndReturnValue(state).map(_.asRight[Int]) - - case state: EntryState.Loading[F, V] => - releaseAndReturnLoading(state).map(_.asRight[Int]) - - // `Removed` means that this entry won't be present in the map - // next time we look the key up (see `remove` flow), - // so we just retry. - case EntryState.Removed => - (counter + 1) - .asLeft[Either[A, Either[F[V], V]]] - .pure[F] - } - .uncancelable - } - } + entryRef <- Ref[F].of[EntryState[F, V]]( + EntryState.Loading(deferred) + ) + result <- set(entryRefs.updated(key, entryRef)).flatMap { + case true => + value + .map { case (a, value, release) => + val entry = entryOf(value, release) + (a, entry) + } + .attempt + .race1 { deferred.get } + .flatMap { + // `value` got computed, and deferred was not (yet) completed by any other fiber in `put` + case Left(Right((a, entry))) => + deferred + .complete(entry.asRight) + .flatMap { + // Successfully completed our deferred, + // now trying to place the new value in the entry. + case true => + def releaseAndReturnValue( + state: EntryState.Value[F, V] + ): F[Either[A, Either[F[V], V]]] = + entry.release1.start + .as { + state.entry.value + .asRight[F[V]] + .asRight[A] } - entryRef - .access - .flatMap { - // Entry is still in loading state, containing the same deferred we just completed. - // Now we can try to put the computed value in the same entryRef. - case (state: EntryState.Loading[F, V], set) if state.deferred == deferred => - set(EntryState.Value(entry)) - .flatMap { - // Happy path: successfully placed our computed value - case true => - a - .asLeft[Either[F[V], V]] - .pure[F] - // Failed to set our value, meaning the entry was either: - // - Updated: in that case we release our computed value, and return - // the value (or its computation), giving it the priority - // - Removed: in that case we try to put our value back in the map - case false => - entryRef - .get - .flatMap { - case state: EntryState.Value[F, V] => - releaseAndReturnValue(state) - - case state: EntryState.Loading[F, V] => - releaseAndReturnLoading(state) - - case EntryState.Removed => - tryPutNewValue - } - } - - case (state: EntryState.Value[F, V], _) => - releaseAndReturnValue(state) - - case (state: EntryState.Loading[F, V], _) => - releaseAndReturnLoading(state) - - case (EntryState.Removed, _) => - tryPutNewValue + def releaseAndReturnLoading( + state: EntryState.Loading[F, V] + ): F[Either[A, Either[F[V], V]]] = + entry.release1.start + .as { + state.deferred.getOrError + .map(_.value) + .asLeft[V] + .asRight[A] } - // Deferred got completed by another fiber, so we return what they put there, - // and release the value we just computed. - case false => - entry - .release1 - .start - .productR( - deferred - .getOrError - .map { entry => - entry - .value - .asRight[F[V]] - .asRight[A] - } - ) - } - - // `value` computation completed with error, - // and deferred was not completed in another fiber in `put`. - case Left(Left(error)) => - deferred - .complete(error.asLeft) - .flatMap { - // Successfully completed our deferred with error, - // now trying to remove the entry from the map, if it is still there. - case true => - 0.tailRecM { counter1 => - ref - .access + // Try putting computed value in the map, if there is no entry with our key. + // If the map already contains an entry with our key, + // return its value (or value computation). + def tryPutNewValue + : F[Either[A, Either[F[V], V]]] = + 0.tailRecM { counter => + ref.access .flatMap { case (entryRefs, set) => entryRefs .get(key) .fold { - // Key was removed while we were loading, - // so we are just propagating the error - error.raiseError[F, Either[Int, Either[F[V], V]]] - } { - // The entry we added to the map is still there and unmodified, - // so we can safely remove it and propagate the error - case `entryRef` => - set(entryRefs - key).flatMap { - // Happy path: successfully removed our entry - case true => - error.raiseError[F, Either[Int, Either[F[V], V]]] - // Retrying (different keys could've been modified in the map) - case false => - (counter1 + 1) - .asLeft[Either[F[V], V]] - .pure[F] + // No entry present in the map, so we try to add a new one + Ref[F] + .of[EntryState[F, V]]( + EntryState.Value(entry) + ) + .flatMap { entryRef => + set( + entryRefs + .updated(key, entryRef) + ).map { + case true => + a + .asLeft[Either[F[ + V + ], V]] + .asRight[Int] + case false => + (counter + 1) + .asLeft[Either[ + A, + Either[F[V], V] + ]] + } } - // Another fiber replaced the `ref` we added to the map, - // so we return their value (computed or ongoing), - // or propagate our error if our entry got removed. - case entryRef => - entryRef - .optEither - .flatMap(_.liftTo[F](error)) - .map(_.asRight[Int]) + } { entryRef => + entryRef.get.flatMap { + case state: EntryState.Value[ + F, + V + ] => + releaseAndReturnValue(state) + .map(_.asRight[Int]) + + case state: EntryState.Loading[ + F, + V + ] => + releaseAndReturnLoading( + state + ).map(_.asRight[Int]) + + // `Removed` means that this entry won't be present in the map + // next time we look the key up (see `remove` flow), + // so we just retry. + case EntryState.Removed => + (counter + 1) + .asLeft[Either[ + A, + Either[F[V], V] + ]] + .pure[F] + }.uncancelable } } } - // Someone else completed the deferred before us, so they must've take care of - // updating the `ref`, and we return their result. - case false => - deferred - .getOrError - .map { _.value } - .asLeft[V] - .pure[F] - } - .map { _.asRight[A] } + entryRef.access + .flatMap { + // Entry is still in loading state, containing the same deferred we just completed. + // Now we can try to put the computed value in the same entryRef. + case ( + state: EntryState.Loading[F, V], + set + ) if state.deferred == deferred => + set(EntryState.Value(entry)) + .flatMap { + // Happy path: successfully placed our computed value + case true => + a + .asLeft[Either[F[V], V]] + .pure[F] + // Failed to set our value, meaning the entry was either: + // - Updated: in that case we release our computed value, and return + // the value (or its computation), giving it the priority + // - Removed: in that case we try to put our value back in the map + case false => + entryRef.get + .flatMap { + case state: EntryState.Value[ + F, + V + ] => + releaseAndReturnValue( + state + ) + + case state: EntryState.Loading[ + F, + V + ] => + releaseAndReturnLoading( + state + ) + + case EntryState.Removed => + tryPutNewValue + } + } - // Deferred was completed by `put` in another fiber before `value` computation completed. - // We return their value, and schedule release of our value that is still being computed. - case Right((fiber, entry)) => - fiber - .joinWithNever - .flatMap { - case Right((_, entry)) => entry.release1 - case _ => ().pure[F] - } - .start - .productR { - entry - .liftTo[F] - .map { entry => - entry - .value - .asRight[F[V]] - .asRight[A] + case ( + state: EntryState.Value[F, V], + _ + ) => + releaseAndReturnValue(state) + + case ( + state: EntryState.Loading[F, V], + _ + ) => + releaseAndReturnLoading(state) + + case (EntryState.Removed, _) => + tryPutNewValue } - } - } - .map { _.asRight[Int] } - case false => - (counter + 1) - .asLeft[Either[A, Either[F[V], V]]] - .pure[F] - } - .uncancelable + // Deferred got completed by another fiber, so we return what they put there, + // and release the value we just computed. + case false => + entry.release1.start + .productR( + deferred.getOrError + .map { entry => + entry.value + .asRight[F[V]] + .asRight[A] + } + ) + } + + // `value` computation completed with error, + // and deferred was not completed in another fiber in `put`. + case Left(Left(error)) => + deferred + .complete(error.asLeft) + .flatMap { + // Successfully completed our deferred with error, + // now trying to remove the entry from the map, if it is still there. + case true => + 0.tailRecM { counter1 => + ref.access + .flatMap { case (entryRefs, set) => + entryRefs + .get(key) + .fold { + // Key was removed while we were loading, + // so we are just propagating the error + error.raiseError[F, Either[ + Int, + Either[F[V], V] + ]] + } { + // The entry we added to the map is still there and unmodified, + // so we can safely remove it and propagate the error + case `entryRef` => + set(entryRefs - key).flatMap { + // Happy path: successfully removed our entry + case true => + error.raiseError[F, Either[ + Int, + Either[F[V], V] + ]] + // Retrying (different keys could've been modified in the map) + case false => + (counter1 + 1) + .asLeft[Either[F[V], V]] + .pure[F] + } + // Another fiber replaced the `ref` we added to the map, + // so we return their value (computed or ongoing), + // or propagate our error if our entry got removed. + case entryRef => + entryRef.optEither + .flatMap(_.liftTo[F](error)) + .map(_.asRight[Int]) + } + } + } + + // Someone else completed the deferred before us, so they must've take care of + // updating the `ref`, and we return their result. + case false => + deferred.getOrError + .map { _.value } + .asLeft[V] + .pure[F] + } + .map { _.asRight[A] } + + // Deferred was completed by `put` in another fiber before `value` computation completed. + // We return their value, and schedule release of our value that is still being computed. + case Right((fiber, entry)) => + fiber.joinWithNever + .flatMap { + case Right((_, entry)) => entry.release1 + case _ => ().pure[F] + } + .start + .productR { + entry + .liftTo[F] + .map { entry => + entry.value + .asRight[F[V]] + .asRight[A] + } + } + } + .map { _.asRight[Int] } + + case false => + (counter + 1) + .asLeft[Either[A, Either[F[V], V]]] + .pure[F] + }.uncancelable } yield result } { entryRef => // Map already contained an entry under our key, so we return that value (or its ongoing computation) - entryRef - .optEither + entryRef.optEither .map { case Some(either) => either @@ -370,8 +394,7 @@ private[scache] object LoadingCache { def put(key: K, value: V, release: Option[Release]): F[F[Option[V]]] = { val entry = entryOf(value, release) 0.tailRecM { counter => - ref - .access + ref.access .flatMap { case (entryRefs, set) => entryRefs .get(key) @@ -391,101 +414,98 @@ private[scache] object LoadingCache { } } } { entryRef => - entryRef - .access - .flatMap { - // A computed value is already present in the map, so we are replacing it with our value. - case (state: EntryState.Value[F, V], set) => - set(EntryState.Value(entry)) - .flatMap { - // Successfully replaced the entryRef with our value, - // now we are responsible for releasing the old value. - case true => - state - .entry - .release - .traverse { _.start } - .map { fiber => - fiber - .foldMapM { _.joinWithNever } - .as { state.entry.value.some } - .asRight[Int] - } - // Failed to set the entryRef to our value - // so we just release our value and exit. - case false => - entry - .release - .traverse { _.start } // Start releasing and forget - .as { - none[V] - .pure[F] - .asRight[Int] - } - } + entryRef.access.flatMap { + // A computed value is already present in the map, so we are replacing it with our value. + case (state: EntryState.Value[F, V], set) => + set(EntryState.Value(entry)) + .flatMap { + // Successfully replaced the entryRef with our value, + // now we are responsible for releasing the old value. + case true => + state.entry.release + .traverse { _.start } + .map { fiber => + fiber + .foldMapM { _.joinWithNever } + .as { state.entry.value.some } + .asRight[Int] + } + // Failed to set the entryRef to our value + // so we just release our value and exit. + case false => + entry.release + .traverse { + _.start + } // Start releasing and forget + .as { + none[V] + .pure[F] + .asRight[Int] + } + } - // The value is still loading, so we first try to complete the deferred with it, - // and then replace it with our value. - case (state: EntryState.Loading[F, V], set) => - state - .deferred - .complete(entry.asRight) - .flatMap { - // We successfully completed the deferred, now trying to set the value. - case true => - set(EntryState.Value(entry)).flatMap { - // We successfully replaced the entry with our value, so we are done. - case true => - none[V] - .pure[F] - .asRight[Int] - .pure[F] - // Another fiber placed their new value before us - // so we just release our value and exit. - case false => - entry - .release - .traverse { _.start } // Start releasing and forget - .as { - none[V] - .pure[F] - .asRight[Int] - } + // The value is still loading, so we first try to complete the deferred with it, + // and then replace it with our value. + case (state: EntryState.Loading[F, V], set) => + state.deferred + .complete(entry.asRight) + .flatMap { + // We successfully completed the deferred, now trying to set the value. + case true => + set(EntryState.Value(entry)).flatMap { + // We successfully replaced the entry with our value, so we are done. + case true => + none[V] + .pure[F] + .asRight[Int] + .pure[F] + // Another fiber placed their new value before us + // so we just release our value and exit. + case false => + entry.release + .traverse { + _.start + } // Start releasing and forget + .as { + none[V] + .pure[F] + .asRight[Int] + } + } + // Someone just completed the deferred we saw + // so we just release our value and exit. + case false => + entry.release + .traverse { + _.start + } // Start releasing and forget + .as { + none[V] + .pure[F] + .asRight[Int] } - // Someone just completed the deferred we saw - // so we just release our value and exit. - case false => - entry - .release - .traverse { _.start } // Start releasing and forget - .as { - none[V] - .pure[F] - .asRight[Int] - } - } + } - // The key was just removed from the map, so just release the value and exit. - case (EntryState.Removed, _) => - entry - .release - .traverse { _.start } // Start releasing and forget - .as { - none[V] - .pure[F] - .asRight[Int] - } - } - .uncancelable + // The key was just removed from the map, so just release the value and exit. + case (EntryState.Removed, _) => + entry.release + .traverse { _.start } // Start releasing and forget + .as { + none[V] + .pure[F] + .asRight[Int] + } + }.uncancelable } } } } - override def modify[A](key: K)(f: Option[V] => (A, Directive[F, V])): F[(A, Option[F[Unit]])] = { + override def modify[A]( + key: K + )(f: Option[V] => (A, Directive[F, V])): F[(A, Option[F[Unit]])] = { 0.tailRecM { counter => - ref - .access + ref.access .flatMap { case (entryRefs, setMap) => entryRefs .get(key) @@ -494,7 +514,9 @@ private[scache] object LoadingCache { // No entry present in the map, and we want to add a new one case (a, put: Directive.Put[F, V]) => Ref[F] - .of[EntryState[F, V]](EntryState.Value(entryOf(put.value, put.release))) + .of[EntryState[F, V]]( + EntryState.Value(entryOf(put.value, put.release)) + ) .flatMap { entryRef => setMap(entryRefs.updated(key, entryRef)).map { case true => @@ -514,131 +536,128 @@ private[scache] object LoadingCache { } } { entryRef => 0.tailRecM { counter1 => - entryRef - .access - .flatMap { - // A value is already present in the map - case (state: EntryState.Value[F, V], setRef) => - f(state.entry.value.some) match { - case (a, put: Directive.Put[F, V]) => - setRef(EntryState.Value(entryOf(put.value, put.release))) - .flatMap { - // Successfully replaced the entryRef with our value, - // now we are responsible for releasing the old value. - case true => - state - .entry - .release - .traverse { _.start } - .map { release => - (a, release.map(_.joinWithNever)) - .asRight[Int] - .asRight[Int] + entryRef.access.flatMap { + // A value is already present in the map + case (state: EntryState.Value[F, V], setRef) => + f(state.entry.value.some) match { + case (a, put: Directive.Put[F, V]) => + setRef( + EntryState.Value(entryOf(put.value, put.release)) + ) + .flatMap { + // Successfully replaced the entryRef with our value, + // now we are responsible for releasing the old value. + case true => + state.entry.release + .traverse { _.start } + .map { release => + (a, release.map(_.joinWithNever)) + .asRight[Int] + .asRight[Int] + } + // Failed updating entryRef, retrying + case false => + (counter1 + 1) + .asLeft[Either[Int, (A, Option[F[Unit]])]] + .pure[F] + } + // Keeping the value intact and exiting + case (a, Directive.Ignore) => + (a, none[F[Unit]]) + .asRight[Int] + .asRight[Int] + .pure[F] + // Removing the value + case (a, Directive.Remove) => + setRef(EntryState.Removed) + .flatMap { + // Successfully set the entryRef to `Removed` state, now removing it from the map. + // Only removing the key if it still contains this entry, otherwise noop. + case true => + ref + .update { entryRefs => + entryRefs.get(key) match { + case Some(`entryRef`) => entryRefs - key + case _ => entryRefs } - // Failed updating entryRef, retrying - case false => - (counter1 + 1) - .asLeft[Either[Int, (A, Option[F[Unit]])]] - .pure[F] - } - // Keeping the value intact and exiting - case (a, Directive.Ignore) => - (a, none[F[Unit]]) - .asRight[Int] - .asRight[Int] - .pure[F] - // Removing the value - case (a, Directive.Remove) => - setRef(EntryState.Removed) - .flatMap { - // Successfully set the entryRef to `Removed` state, now removing it from the map. - // Only removing the key if it still contains this entry, otherwise noop. - case true => - ref - .update { entryRefs => - entryRefs.get(key) match { - case Some(`entryRef`) => entryRefs - key - case _ => entryRefs + } + .flatMap { _ => + // Releasing the value regardless of the map update result. + state.entry.release + .traverse { _.start } + .map { release => + (a, release.map(_.joinWithNever)) + .asRight[Int] + .asRight[Int] } - } - .flatMap { _ => - // Releasing the value regardless of the map update result. - state - .entry - .release - .traverse { _.start } - .map { release => - (a, release.map(_.joinWithNever)) - .asRight[Int] - .asRight[Int] - } - } - // Failed updating entryRef, retrying - case false => - (counter1 + 1) - .asLeft[Either[Int, (A, Option[F[Unit]])]] - .pure[F] - } - } - - // Entry in the map is still loading - case (state: EntryState.Loading[F, V], setRef) => - f(None) match { - // Trying to replace it with our value - case (a, put: Directive.Put[F, V]) => - val entry = entryOf(put.value, put.release) - state - .deferred - .complete(entry.asRight) - .flatMap { - // We successfully completed the deferred, now trying to set the value. - case true => - setRef(EntryState.Value(entry)).map { - // We successfully replaced the entry with our value, so we are done. - case true => - (a, none[F[Unit]]) - .asRight[Int] - .asRight[Int] - // Another fiber placed their new value (only Removed should be possible) - // before us so we retry accessing the entry. - case false => - (counter1 + 1) - .asLeft[Either[Int, (A, Option[F[Unit]])]] } - // Failed to complete the deferred, meaning someone else completed it, and will - // now set the new value in the entryRef. Retrying the lookup. - case false => - (counter1 + 1) - .asLeft[Either[Int, (A, Option[F[Unit]])]] - .pure[F] - } - // Noop decision, exiting - case (a, Directive.Ignore | Directive.Remove) => - (a, none[F[Unit]]) - .asRight[Int] - .asRight[Int] - .pure[F] - } + // Failed updating entryRef, retrying + case false => + (counter1 + 1) + .asLeft[Either[Int, (A, Option[F[Unit]])]] + .pure[F] + } + } - // Entry was just removed, it soon will be gone from the map. - case (EntryState.Removed, _) => - f(None) match { - // We want to place the new value; - // Retrying the map lookup, expecting a different result for our key. - case (_, _: Directive.Put[F, V]) => - (counter + 1) - .asLeft[(A, Option[F[Unit]])] - .asRight[Int] - .pure[F] - // Noop decision, exiting - case (a, Directive.Ignore | Directive.Remove) => - (a, none[F[Unit]]) - .asRight[Int] - .asRight[Int] - .pure[F] - } - } - .uncancelable + // Entry in the map is still loading + case (state: EntryState.Loading[F, V], setRef) => + f(None) match { + // Trying to replace it with our value + case (a, put: Directive.Put[F, V]) => + val entry = entryOf(put.value, put.release) + state.deferred + .complete(entry.asRight) + .flatMap { + // We successfully completed the deferred, now trying to set the value. + case true => + setRef(EntryState.Value(entry)).map { + // We successfully replaced the entry with our value, so we are done. + case true => + (a, none[F[Unit]]) + .asRight[Int] + .asRight[Int] + // Another fiber placed their new value (only Removed should be possible) + // before us so we retry accessing the entry. + case false => + (counter1 + 1) + .asLeft[Either[ + Int, + (A, Option[F[Unit]]) + ]] + } + // Failed to complete the deferred, meaning someone else completed it, and will + // now set the new value in the entryRef. Retrying the lookup. + case false => + (counter1 + 1) + .asLeft[Either[Int, (A, Option[F[Unit]])]] + .pure[F] + } + // Noop decision, exiting + case (a, Directive.Ignore | Directive.Remove) => + (a, none[F[Unit]]) + .asRight[Int] + .asRight[Int] + .pure[F] + } + + // Entry was just removed, it soon will be gone from the map. + case (EntryState.Removed, _) => + f(None) match { + // We want to place the new value; + // Retrying the map lookup, expecting a different result for our key. + case (_, _: Directive.Put[F, V]) => + (counter + 1) + .asLeft[(A, Option[F[Unit]])] + .asRight[Int] + .pure[F] + // Noop decision, exiting + case (a, Directive.Ignore | Directive.Remove) => + (a, none[F[Unit]]) + .asRight[Int] + .asRight[Int] + .pure[F] + } + }.uncancelable } } } @@ -646,29 +665,22 @@ private[scache] object LoadingCache { } def contains(key: K) = { - ref - .get + ref.get .map { _.contains(key) } } - def size = { - ref - .get + ref.get .map { _.size } } - def keys = { - ref - .get + ref.get .map { _.keySet } } - def values = { - ref - .get + ref.get .flatMap { entryRefs => entryRefs .foldLeft { @@ -677,11 +689,10 @@ private[scache] object LoadingCache { .pure[F] } { case (values, (key, entryRef)) => values.flatMap { values => - entryRef - .value + entryRef.value .map { case Some(value) => (key, value) :: values - case None => values + case None => values } } } @@ -690,8 +701,7 @@ private[scache] object LoadingCache { } def values1 = { - ref - .get + ref.get .flatMap { entryRefs => entryRefs .foldLeft { @@ -700,11 +710,10 @@ private[scache] object LoadingCache { .pure[F] } { case (values, (key, entryRef)) => values.flatMap { values => - entryRef - .optEither + entryRef.optEither .map { case Some(value) => (key, value) :: values - case None => values + case None => values } } } @@ -712,11 +721,9 @@ private[scache] object LoadingCache { .map { _.toMap } } - def remove(key: K): F[F[Option[V]]] = { 0.tailRecM { counter => - ref - .access + ref.access .flatMap { case (entryRefs, set) => entryRefs .get(key) @@ -726,77 +733,66 @@ private[scache] object LoadingCache { .asRight[Int] .pure[F] } { entryRef => - set(entryRefs - key) - .flatMap { - case true => - // We just removed the entry for the map, now we need to release it. - // Replacing the value of the ref with `Removed` means that we are getting responsible for the release. - entryRef - .getAndSet(EntryState.Removed) - .flatMap { - // We removed a loaded value, so we are responsible for releasing it. - case state: EntryState.Value[F, V] => - state - .entry - .release1 - .as { state.entry.value.some } - .start - .map { fiber => - fiber - .joinWithNever - .asRight[Int] - } + set(entryRefs - key).flatMap { + case true => + // We just removed the entry for the map, now we need to release it. + // Replacing the value of the ref with `Removed` means that we are getting responsible for the release. + entryRef + .getAndSet(EntryState.Removed) + .flatMap { + // We removed a loaded value, so we are responsible for releasing it. + case state: EntryState.Value[F, V] => + state.entry.release1 + .as { state.entry.value.some } + .start + .map { fiber => + fiber.joinWithNever + .asRight[Int] + } - // We removed a loading value, and the fiber that will complete it will also - // release that value, so there is nothing for us to return. - case _: EntryState.Loading[F, V] => - none[V] - .pure[F] - .asRight[Int] - .pure[F] + // We removed a loading value, and the fiber that will complete it will also + // release that value, so there is nothing for us to return. + case _: EntryState.Loading[F, V] => + none[V] + .pure[F] + .asRight[Int] + .pure[F] - // We removed an entry that was already being removed by another fiber, so we are done. - case EntryState.Removed => - none[V] - .pure[F] - .asRight[Int] - .pure[F] - } - case false => - (counter + 1) - .asLeft[F[Option[V]]] - .pure[F] - } - .uncancelable + // We removed an entry that was already being removed by another fiber, so we are done. + case EntryState.Removed => + none[V] + .pure[F] + .asRight[Int] + .pure[F] + } + case false => + (counter + 1) + .asLeft[F[Option[V]]] + .pure[F] + }.uncancelable } } } } - def clear = { ref .getAndSet(EntryRefs.empty) .flatMap { entryRefs => - entryRefs - .parFoldMap1 { case (_, entryRef) => - entryRef - .getOption - .flatMap { _.foldMapM { _.release1 } } - .uncancelable - } - .start + entryRefs.parFoldMap1 { case (_, entryRef) => + entryRef.getOption.flatMap { + _.foldMapM { _.release1 } + }.uncancelable + }.start } .uncancelable .map { _.joinWithNever } } def foldMap[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = { - ref - .get + ref.get .flatMap { entryRefs => - val zero = CommutativeMonoid[A] - .empty + val zero = CommutativeMonoid[A].empty .pure[F] entryRefs.foldLeft(zero) { case (a, (key, entryRef)) => for { @@ -811,23 +807,22 @@ private[scache] object LoadingCache { } def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = { - ref - .get + ref.get .flatMap { entryRefs => Parallel[F].sequential { - val zero = Parallel[F] - .applicative + val zero = Parallel[F].applicative .pure(CommutativeMonoid[A].empty) entryRefs .foldLeft(zero) { case (a, (key, entryRef)) => val b = Parallel[F].parallel { for { v <- entryRef.optEither - b <- v.fold(CommutativeMonoid[A].empty.pure[F])(v => f(key, v)) + b <- v.fold(CommutativeMonoid[A].empty.pure[F])(v => + f(key, v) + ) } yield b } - Parallel[F] - .applicative + Parallel[F].applicative .map2(a, b)(CommutativeMonoid[A].combine) } } @@ -846,7 +841,9 @@ private[scache] object LoadingCache { sealed trait EntryState[+F[_], +A] object EntryState { - final case class Loading[F[_], A](deferred: Deferred[F, Either[Throwable, Entry[F, A]]]) extends EntryState[F, A] + final case class Loading[F[_], A]( + deferred: Deferred[F, Either[Throwable, Entry[F, A]]] + ) extends EntryState[F, A] final case class Value[F[_], A](entry: Entry[F, A]) extends EntryState[F, A] case object Removed extends EntryState[Nothing, Nothing] } @@ -861,10 +858,10 @@ private[scache] object LoadingCache { def empty[F[_], K, V]: EntryRefs[F, K, V] = Map.empty } - implicit class DeferredThrowOps[F[_], A](val self: DeferredThrow[F, A]) extends AnyVal { + implicit class DeferredThrowOps[F[_], A](val self: DeferredThrow[F, A]) + extends AnyVal { def getOrError(implicit F: MonadThrow[F]): F[A] = { - self - .get + self.get .flatMap { case Right(a) => a.pure[F] case Left(a) => a.raiseError[F, A] @@ -872,31 +869,35 @@ private[scache] object LoadingCache { } def getOption(implicit F: Functor[F]): F[Option[A]] = { - self - .get + self.get .map { _.toOption } } } - implicit class EntryStateOps[F[_], A](val self: EntryState[F, A]) extends AnyVal { + implicit class EntryStateOps[F[_], A](val self: EntryState[F, A]) + extends AnyVal { - def getOption(implicit F: Applicative[F]): F[Option[Entry[F, A]]] ={ + def getOption(implicit F: Applicative[F]): F[Option[Entry[F, A]]] = { self match { - case EntryState.Loading(deferred: Deferred[F, Either[Throwable, Entry[F, A]]]) => deferred.getOption + case EntryState.Loading( + deferred: Deferred[F, Either[Throwable, Entry[F, A]]] + ) => + deferred.getOption case EntryState.Value(entry) => entry.some.pure[F] - case EntryState.Removed => none[Entry[F, A]].pure[F] - }} + case EntryState.Removed => none[Entry[F, A]].pure[F] + } + } def optEither(implicit F: MonadThrow[F]): Option[Either[F[A], A]] = self match { case EntryState.Value(entry) => - entry - .value + entry.value .asRight[F[A]] .some - case EntryState.Loading(deferred: Deferred[F, Either[Throwable, Entry[F, A]]]) => - deferred - .getOrError + case EntryState.Loading( + deferred: Deferred[F, Either[Throwable, Entry[F, A]]] + ) => + deferred.getOrError .map(_.value) .asLeft[A] .some @@ -909,31 +910,26 @@ private[scache] object LoadingCache { implicit class EntryRefOps[F[_], A](val self: EntryRef[F, A]) extends AnyVal { def getOption(implicit F: Monad[F]): F[Option[Entry[F, A]]] = { - self - .get + self.get .flatMap(_.getOption) } def optEither(implicit F: MonadThrow[F]): F[Option[Either[F[A], A]]] = { - self - .get + self.get .map(_.optEither) } def value(implicit F: MonadThrow[F]): F[Option[F[A]]] = { - self - .get + self.get .map { - case EntryState.Value(entry) => - entry - .value + case EntryState.Value(entry) => + entry.value .pure[F] .some - case EntryState.Loading(deferred: Deferred[F, Either[Throwable, Entry[F, A]]]) => - deferred - .getOrError - .map { _.value } - .some + case EntryState.Loading( + deferred: Deferred[F, Either[Throwable, Entry[F, A]]] + ) => + deferred.getOrError.map { _.value }.some case EntryState.Removed => none[F[A]] } @@ -941,8 +937,7 @@ private[scache] object LoadingCache { def update1(f: A => A)(implicit F: Monad[F]): F[Unit] = { 0.tailRecM { counter => - self - .access + self.access .flatMap { case (EntryState.Value(entry), set) => val entry1 = entry.copy(value = f(entry.value)) @@ -964,42 +959,40 @@ private[scache] object LoadingCache { } implicit class Ops[F[_], A, E](val fa: F[A]) extends AnyVal { - def race1[B]( - fb: F[B])(implicit - F: GenConcurrent[F, E] + def race1[B](fb: F[B])(implicit + F: GenConcurrent[F, E] ): F[Either[A, (Fiber[F, E, A], B)]] = { import F.* uncancelable { poll => poll(racePair(fa, fb)).flatMap { - case Left((a, fiber)) => + case Left((a, fiber)) => a match { case Outcome.Succeeded(a) => - fiber - .cancel + fiber.cancel .productR { a } .map { _.asLeft } - case Outcome.Errored(a) => - fiber - .cancel + case Outcome.Errored(a) => + fiber.cancel .productR { raiseError(a) } - case Outcome.Canceled() => + case Outcome.Canceled() => poll(canceled) *> never } case Right((fiber, b)) => b match { case Outcome.Succeeded(b) => b.map { b => (fiber, b).asRight[A] } case Outcome.Errored(eb) => raiseError(eb) - case Outcome.Canceled() => + case Outcome.Canceled() => poll(fiber.join) .onCancel(fiber.cancel) .flatMap { - case Outcome.Succeeded(a) => a.map { _.asLeft[(Fiber[F, E, A], B)] } - case Outcome.Errored(a) => raiseError(a) - case Outcome.Canceled() => poll(canceled) *> never + case Outcome.Succeeded(a) => + a.map { _.asLeft[(Fiber[F, E, A], B)] } + case Outcome.Errored(a) => raiseError(a) + case Outcome.Canceled() => poll(canceled) *> never } } } } } } -} \ No newline at end of file +} diff --git a/src/main/scala/com/evolution/scache/NrOfPartitions.scala b/src/main/scala/com/evolution/scache/NrOfPartitions.scala index f8d5de5..5cbf4a0 100644 --- a/src/main/scala/com/evolution/scache/NrOfPartitions.scala +++ b/src/main/scala/com/evolution/scache/NrOfPartitions.scala @@ -4,12 +4,10 @@ import cats.FlatMap import cats.syntax.all.* import com.evolutiongaming.catshelper.Runtime - object NrOfPartitions { def apply[F[_]: FlatMap: Runtime](): F[Int] = { - Runtime[F] - .availableCores + Runtime[F].availableCores .map { _ + 1 } } } diff --git a/src/main/scala/com/evolution/scache/PartitionedCache.scala b/src/main/scala/com/evolution/scache/PartitionedCache.scala index df92378..ae85b89 100644 --- a/src/main/scala/com/evolution/scache/PartitionedCache.scala +++ b/src/main/scala/com/evolution/scache/PartitionedCache.scala @@ -8,7 +8,7 @@ import cats.kernel.{CommutativeMonoid, Monoid} object PartitionedCache { def apply[F[_]: MonadThrow: Parallel, K, V]( - partitions: Partitions[K, Cache[F, K, V]] + partitions: Partitions[K, Cache[F, K, V]] ): Cache[F, K, V] = { implicit def monoidUnit: Monoid[F[Unit]] = Applicative.monoid[F, Unit] @@ -47,7 +47,9 @@ object PartitionedCache { .put(key, value, release) } - def modify[A](key: K)(f: Option[V] => (A, Cache.Directive[F, V])): F[(A, Option[F[Unit]])] = { + def modify[A]( + key: K + )(f: Option[V] => (A, Cache.Directive[F, V])): F[(A, Option[F[Unit]])] = { partitions .get(key) .modify(key)(f) @@ -60,37 +62,30 @@ object PartitionedCache { } def size = { - partitions - .values + partitions.values .foldMapM(_.size) } def keys = { - partitions - .values + partitions.values .foldLeftM(Set.empty[K]) { (keys, cache) => - cache - .keys + cache.keys .map { _ ++ keys } } } def values = { - partitions - .values + partitions.values .foldLeftM(Map.empty[K, F[V]]) { (values, cache) => - cache - .values + cache.values .map { _ ++ values } } } def values1 = { - partitions - .values + partitions.values .foldLeftM(Map.empty[K, Either[F[V], V]]) { (values, cache) => - cache - .values1 + cache.values1 .map { _ ++ values } } } @@ -102,22 +97,19 @@ object PartitionedCache { } def clear = { - partitions - .values + partitions.values .foldMapM { _.clear } } def foldMap[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = { - partitions - .values + partitions.values .foldMapM { _.foldMap(f) } } def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = { - partitions - .values + partitions.values .parFoldMap1 { _.foldMap(f) } } } } -} \ No newline at end of file +} diff --git a/src/main/scala/com/evolution/scache/Partitions.scala b/src/main/scala/com/evolution/scache/Partitions.scala index 030aada..4c63546 100644 --- a/src/main/scala/com/evolution/scache/Partitions.scala +++ b/src/main/scala/com/evolution/scache/Partitions.scala @@ -15,7 +15,6 @@ object Partitions { type Partition = Int - def const[K, V](value: V): Partitions[K, V] = new Partitions[K, V] { def get(key: K) = value @@ -23,22 +22,20 @@ object Partitions { val values = List(value) } - def of[F[_] : Monad, K : Hash, V]( - nrOfPartitions: Int, - valueOf: Partition => F[V] + def of[F[_]: Monad, K: Hash, V]( + nrOfPartitions: Int, + valueOf: Partition => F[V] ): F[Partitions[K, V]] = { if (nrOfPartitions <= 1) { valueOf(0).map(const) } else { - (0 until nrOfPartitions) - .toVector + (0 until nrOfPartitions).toVector .traverse { partition => valueOf(partition) } .map { partitions => Partitions[K, V](partitions) } } } - - private def apply[K : Hash, V](partitions: Vector[V]): Partitions[K, V] = { + private def apply[K: Hash, V](partitions: Vector[V]): Partitions[K, V] = { val nrOfPartitions = partitions.size @@ -53,4 +50,4 @@ object Partitions { val values = partitions.toList } } -} \ No newline at end of file +} diff --git a/src/main/scala/com/evolution/scache/SerialMap.scala b/src/main/scala/com/evolution/scache/SerialMap.scala index 0ddd0b8..066e397 100644 --- a/src/main/scala/com/evolution/scache/SerialMap.scala +++ b/src/main/scala/com/evolution/scache/SerialMap.scala @@ -6,9 +6,7 @@ import cats.effect.implicits.* import cats.syntax.all.* import com.evolutiongaming.catshelper.{Runtime, SerialRef} - -/** - * Map-like data structure, which runs updates serially for the same key +/** Map-like data structure, which runs updates serially for the same key */ trait SerialMap[F[_], K, V] { @@ -20,13 +18,13 @@ trait SerialMap[F[_], K, V] { def put(key: K, value: V): F[Option[V]] - /** - * `f` will be run serially for the same key, entry will be removed in case of `f` returns `none` + /** `f` will be run serially for the same key, entry will be removed in case + * of `f` returns `none` */ def modify[A](key: K)(f: Option[V] => F[(Option[V], A)]): F[A] - /** - * `f` will be run serially for the same key, entry will be removed in case of `f` returns `none` + /** `f` will be run serially for the same key, entry will be removed in case + * of `f` returns `none` */ def update[A](key: K)(f: Option[V] => F[Option[V]]): F[Unit] @@ -34,8 +32,7 @@ trait SerialMap[F[_], K, V] { def keys: F[Set[K]] - /** - * Might be an expensive call + /** Might be an expensive call */ def values: F[Map[K, V]] @@ -46,57 +43,59 @@ trait SerialMap[F[_], K, V] { object SerialMap { self => - def empty[F[_] : Applicative, K, V]: SerialMap[F, K, V] = new SerialMap[F, K, V] { - - def get(key: K) = none[V].pure[F] + def empty[F[_]: Applicative, K, V]: SerialMap[F, K, V] = + new SerialMap[F, K, V] { - def getOrElse(key: K, default: => F[V]): F[V] = default + def get(key: K) = none[V].pure[F] - def getOrUpdate(key: K, value: => F[V]): F[V] = value + def getOrElse(key: K, default: => F[V]): F[V] = default - def put(key: K, value: V) = none[V].pure[F] + def getOrUpdate(key: K, value: => F[V]): F[V] = value - def modify[A](key: K)(f: Option[V] => F[(Option[V], A)]) = f(none[V]).map { case (_, value) => value } + def put(key: K, value: V) = none[V].pure[F] - def update[A](key: K)(f: Option[V] => F[Option[V]]) = f(none[V]).void + def modify[A](key: K)(f: Option[V] => F[(Option[V], A)]) = + f(none[V]).map { case (_, value) => value } - def size = 0.pure[F] + def update[A](key: K)(f: Option[V] => F[Option[V]]) = f(none[V]).void - def keys = Set.empty[K].pure[F] + def size = 0.pure[F] - def values = Map.empty[K, V].pure[F] + def keys = Set.empty[K].pure[F] - def remove(key: K) = none[V].pure[F] + def values = Map.empty[K, V].pure[F] - def clear = ().pure[F] - } + def remove(key: K) = none[V].pure[F] + def clear = ().pure[F] + } def apply[F[_]](implicit F: Concurrent[F]): Apply[F] = new Apply(F) + def of[F[_]: Concurrent: Runtime, K, V]: F[SerialMap[F, K, V]] = of(None) - def of[F[_] : Concurrent : Runtime, K, V]: F[SerialMap[F, K, V]] = of(None) - - - def of[F[_] : Concurrent : Runtime, K, V](partitions: Int): F[SerialMap[F, K, V]] = of(Some(partitions)) + def of[F[_]: Concurrent: Runtime, K, V]( + partitions: Int + ): F[SerialMap[F, K, V]] = of(Some(partitions)) - - def of[F[_]: Concurrent: Runtime, K, V](partitions: Option[Int] = None): F[SerialMap[F, K, V]] = { + def of[F[_]: Concurrent: Runtime, K, V]( + partitions: Option[Int] = None + ): F[SerialMap[F, K, V]] = { Cache .loading[F, K, SerialRef[F, State[V]]](partitions) .allocated .map { case (a, _) => apply(a) } } - - def apply[F[_] : Concurrent, K, V](cache: Cache[F, K, SerialRef[F, State[V]]]): SerialMap[F, K, V] = { + def apply[F[_]: Concurrent, K, V]( + cache: Cache[F, K, SerialRef[F, State[V]]] + ): SerialMap[F, K, V] = { new SerialMap[F, K, V] { self => - def get(key: K) = { for { serialRef <- cache.get(key) - state <- serialRef.fold(State.empty[V].pure[F])(_.get) + state <- serialRef.fold(State.empty[V].pure[F])(_.get) } yield state match { case State.Full(value) => value.some case State.Empty => none[V] @@ -104,7 +103,6 @@ object SerialMap { self => } } - def getOrElse(key: K, default: => F[V]) = { get(key).flatMap { case Some(a) => a.pure[F] @@ -112,7 +110,6 @@ object SerialMap { self => } } - def getOrUpdate(key: K, value: => F[V]) = { modify(key) { case Some(stored) => (stored.some, stored).pure[F] @@ -120,26 +117,27 @@ object SerialMap { self => } } - def put(key: K, value: V) = { modify(key) { prev => (value.some, prev).pure[F] } } - def modify[A](key: K)(f: Option[V] => F[(Option[V], A)]) = { def remove = cache.remove(key) def adding(added: Ref[F, Boolean]) = { for { - _ <- added.set(true) + _ <- added.set(true) serialRef <- SerialRef[F].of(State.empty[V]) } yield serialRef } - def modify(serialRef: SerialRef[F, State[V]], added: Ref[F, Boolean]) = { + def modify( + serialRef: SerialRef[F, State[V]], + added: Ref[F, Boolean] + ) = { def modify(state: State[V]) = { @@ -158,8 +156,8 @@ object SerialMap { self => case Left(error) => val fa = for { added <- added.get - _ <- if (added) remove.void else ().pure[F] - a <- error.raiseError[F, A] + _ <- if (added) remove.void else ().pure[F] + a <- error.raiseError[F, A] } yield a (state, fa) } @@ -185,60 +183,54 @@ object SerialMap { self => } for { - added <- Ref[F].of(false) + added <- Ref[F].of(false) serialRef <- cache.getOrUpdate(key) { adding(added) } - a <- modify(serialRef, added).uncancelable + a <- modify(serialRef, added).uncancelable } yield a } - def update[A](key: K)(f: Option[V] => F[Option[V]]) = { modify(key) { value => - for {d <- f(value)} yield { (d, ()) } + for { d <- f(value) } yield { (d, ()) } } } - val size = cache.size - val keys = cache.keys - val values = { for { - map <- cache.values - list <- map.foldLeft(List.empty[(K, V)].pure[F]) { case (values, (key, serialRef)) => - for { - serialRef <- serialRef - value <- serialRef.get - values <- values - } yield { - value match { - case State.Full(value) => (key, value) :: values - case State.Empty => values - case State.Removed => values + map <- cache.values + list <- map.foldLeft(List.empty[(K, V)].pure[F]) { + case (values, (key, serialRef)) => + for { + serialRef <- serialRef + value <- serialRef.get + values <- values + } yield { + value match { + case State.Full(value) => (key, value) :: values + case State.Empty => values + case State.Removed => values + } } - } } } yield { list.toMap } } - def remove(key: K) = { modify(key) { value => (none[V], value).pure[F] } } - val clear = cache.clear.flatten } } - class Apply[F[_]](val F: Concurrent[F]) extends AnyVal { def of[K, V](implicit runtime: Runtime[F]): F[SerialMap[F, K, V]] = { @@ -247,7 +239,6 @@ object SerialMap { self => } } - sealed trait State[+A] object State { @@ -258,7 +249,6 @@ object SerialMap { self => def empty[A]: State[A] = Empty - final case class Full[A](value: A) extends State[A] case object Empty extends State[Nothing] diff --git a/src/test/scala/com/evolution/scache/CacheEmptySpec.scala b/src/test/scala/com/evolution/scache/CacheEmptySpec.scala index 9c98d72..c0cac89 100644 --- a/src/test/scala/com/evolution/scache/CacheEmptySpec.scala +++ b/src/test/scala/com/evolution/scache/CacheEmptySpec.scala @@ -15,7 +15,7 @@ class CacheEmptySpec extends AsyncFunSuite with Matchers { test("get") { val result = for { value <- cache.get(0) - _ <- Sync[IO].delay { value shouldEqual none[Int] } + _ <- Sync[IO].delay { value shouldEqual none[Int] } } yield {} result.run() } @@ -23,10 +23,10 @@ class CacheEmptySpec extends AsyncFunSuite with Matchers { test("getOrElse") { val result = for { value <- cache.getOrElse(0, 1.pure[IO]) - _ <- Sync[IO].delay { value shouldEqual 1 } - _ <- cache.put(0, 2) + _ <- Sync[IO].delay { value shouldEqual 1 } + _ <- cache.put(0, 2) value <- cache.getOrElse(0, 1.pure[IO]) - _ <- Sync[IO].delay { value shouldEqual 1 } + _ <- Sync[IO].delay { value shouldEqual 1 } } yield {} result.run() } @@ -35,71 +35,68 @@ class CacheEmptySpec extends AsyncFunSuite with Matchers { val result = for { value <- cache.put(0, 0) value <- value - _ <- Sync[IO].delay { value shouldEqual none } + _ <- Sync[IO].delay { value shouldEqual none } value <- cache.get(0) - _ <- Sync[IO].delay { value shouldEqual none } + _ <- Sync[IO].delay { value shouldEqual none } value <- cache.put(0, 1) value <- value - _ <- Sync[IO].delay { value shouldEqual none } + _ <- Sync[IO].delay { value shouldEqual none } value <- cache.put(0, 2) value <- value - _ <- Sync[IO].delay { value shouldEqual none } + _ <- Sync[IO].delay { value shouldEqual none } } yield {} result.run() } - test("remove") { val result = for { - _ <- cache.put(0, 0) + _ <- cache.put(0, 0) value <- cache.remove(0) value <- value - _ <- Sync[IO].delay { value shouldEqual none } + _ <- Sync[IO].delay { value shouldEqual none } value <- cache.get(0) - _ <- Sync[IO].delay { value shouldEqual none } + _ <- Sync[IO].delay { value shouldEqual none } } yield {} result.run() } - test("clear") { val result = for { - _ <- cache.put(0, 0) - _ <- cache.put(1, 1) - _ <- cache.clear + _ <- cache.put(0, 0) + _ <- cache.put(1, 1) + _ <- cache.clear value <- cache.get(0) - _ <- Sync[IO].delay { value shouldEqual none } + _ <- Sync[IO].delay { value shouldEqual none } value <- cache.get(1) - _ <- Sync[IO].delay { value shouldEqual none } + _ <- Sync[IO].delay { value shouldEqual none } } yield {} result.run() } - test("getOrUpdate") { val result = for { deferred <- Deferred[IO, Int] - value0 <- cache.getOrUpdateEnsure(0) { deferred.get } - value2 <- cache.getOrUpdate(0)(1.pure[IO]).startEnsure - _ <- deferred.complete(0) - value0 <- value0.joinWithNever - value1 <- value2.joinWithNever - _ <- IO { value0 shouldEqual 0.asRight } - _ <- IO { value1 shouldEqual 1 } + value0 <- cache.getOrUpdateEnsure(0) { deferred.get } + value2 <- cache.getOrUpdate(0)(1.pure[IO]).startEnsure + _ <- deferred.complete(0) + value0 <- value0.joinWithNever + value1 <- value2.joinWithNever + _ <- IO { value0 shouldEqual 0.asRight } + _ <- IO { value1 shouldEqual 1 } } yield {} result.run() } test("keys") { val result = for { - _ <- cache.put(0, 0) - keys = cache.keys + _ <- cache.put(0, 0) + keys = cache.keys keys0 <- keys - _ <- cache.put(1, 1) + _ <- cache.put(1, 1) keys1 <- keys - _ <- cache.put(2, 2) + _ <- cache.put(2, 2) keys2 <- keys - _ <- cache.clear + _ <- cache.clear keys3 <- keys } yield { keys0 shouldEqual Set.empty @@ -111,17 +108,16 @@ class CacheEmptySpec extends AsyncFunSuite with Matchers { result.run() } - test("values") { val result = for { - _ <- cache.put(0, 0) - values = cache.valuesFlatten + _ <- cache.put(0, 0) + values = cache.valuesFlatten values0 <- values - _ <- cache.put(1, 1) + _ <- cache.put(1, 1) values1 <- values - _ <- cache.put(2, 2) + _ <- cache.put(2, 2) values2 <- values - _ <- cache.clear + _ <- cache.clear values3 <- values } yield { values0 shouldEqual Map.empty diff --git a/src/test/scala/com/evolution/scache/CacheFencedTest.scala b/src/test/scala/com/evolution/scache/CacheFencedTest.scala index 05663cf..cf2ae16 100644 --- a/src/test/scala/com/evolution/scache/CacheFencedTest.scala +++ b/src/test/scala/com/evolution/scache/CacheFencedTest.scala @@ -7,7 +7,6 @@ import com.evolution.scache.IOSuite.* import org.scalatest.funsuite.AsyncFunSuite import org.scalatest.matchers.should.Matchers - class CacheFencedTest extends AsyncFunSuite with Matchers { private val cache = Cache @@ -17,8 +16,8 @@ class CacheFencedTest extends AsyncFunSuite with Matchers { test(s"get succeeds after cache is released") { val result = for { cache <- cache.use(_.pure[IO]) - a <- cache.get(0) - _ = a shouldEqual none + a <- cache.get(0) + _ = a shouldEqual none } yield {} result.run() } @@ -26,8 +25,8 @@ class CacheFencedTest extends AsyncFunSuite with Matchers { test("getOrElse succeeds after cache is released") { val result = for { cache <- cache.use(_.pure[IO]) - a <- cache.getOrElse(0, 1.pure[IO]) - _ = a shouldEqual 1 + a <- cache.getOrElse(0, 1.pure[IO]) + _ = a shouldEqual 1 } yield {} result.run() } @@ -35,8 +34,8 @@ class CacheFencedTest extends AsyncFunSuite with Matchers { test(s"put succeeds after cache is released") { val result = for { cache <- cache.use(_.pure[IO]) - a <- cache.put(0, 0).flatten - _ = a shouldEqual none + a <- cache.put(0, 0).flatten + _ = a shouldEqual none } yield {} result.run() } @@ -44,8 +43,8 @@ class CacheFencedTest extends AsyncFunSuite with Matchers { test(s"put releasable fails after cache is released") { val result = for { cache <- cache.use(_.pure[IO]) - a <- cache.put(0, 0, ().pure[IO]).flatten.attempt - _ = a shouldEqual CacheReleasedError.asLeft + a <- cache.put(0, 0, ().pure[IO]).flatten.attempt + _ = a shouldEqual CacheReleasedError.asLeft } yield {} result.run() } @@ -53,8 +52,8 @@ class CacheFencedTest extends AsyncFunSuite with Matchers { test(s"size succeeds after cache is released") { val result = for { cache <- cache.use { cache => cache.put(0, 0).flatten as cache } - a <- cache.size - _ = a shouldEqual 0 + a <- cache.size + _ = a shouldEqual 0 } yield {} result.run() } @@ -62,8 +61,8 @@ class CacheFencedTest extends AsyncFunSuite with Matchers { test(s"remove succeeds after cache is released") { val result = for { cache <- cache.use(_.pure[IO]) - a <- cache.remove(0).flatten - _ = a shouldEqual none + a <- cache.remove(0).flatten + _ = a shouldEqual none } yield {} result.run() } @@ -71,7 +70,7 @@ class CacheFencedTest extends AsyncFunSuite with Matchers { test(s"clear succeeds after cache is released") { val result = for { cache <- cache.use(_.pure[IO]) - _ <- cache.clear.flatten + _ <- cache.clear.flatten } yield {} result.run() } @@ -79,8 +78,8 @@ class CacheFencedTest extends AsyncFunSuite with Matchers { test(s"getOrUpdate succeeds after cache is released") { val result = for { cache <- cache.use(_.pure[IO]) - a <- cache.getOrUpdate(0)(1.pure[IO]) - _ = a shouldEqual 1 + a <- cache.getOrUpdate(0)(1.pure[IO]) + _ = a shouldEqual 1 } yield {} result.run() } @@ -88,8 +87,8 @@ class CacheFencedTest extends AsyncFunSuite with Matchers { test(s"getOrUpdateReleasable fails after cache is released") { val result = for { cache <- cache.use { _.pure[IO] } - a <- cache.getOrUpdate1(0)((1, 1, IO.unit.some).pure[IO]).attempt - _ <- IO { a shouldEqual CacheReleasedError.asLeft } + a <- cache.getOrUpdate1(0)((1, 1, IO.unit.some).pure[IO]).attempt + _ <- IO { a shouldEqual CacheReleasedError.asLeft } } yield {} result.run() } @@ -98,7 +97,7 @@ class CacheFencedTest extends AsyncFunSuite with Matchers { def cache(ref: Ref[IO, Int]) = { val cache = for { - _ <- Resource.release { ref.update(_ + 1) } + _ <- Resource.release { ref.update(_ + 1) } cache <- Cache .loading[IO, Int, Int] .flatMap { _.withFence } @@ -108,12 +107,12 @@ class CacheFencedTest extends AsyncFunSuite with Matchers { val result = for { ref <- Ref[IO].of(0) - ab <- cache(ref).allocated + ab <- cache(ref).allocated (_, release) = ab - _ <- release - _ <- release - a <- ref.get - _ = a shouldEqual 1 + _ <- release + _ <- release + a <- ref.get + _ = a shouldEqual 1 } yield {} result.run() } diff --git a/src/test/scala/com/evolution/scache/CacheSpec.scala b/src/test/scala/com/evolution/scache/CacheSpec.scala index 15edbac..51fe9b7 100644 --- a/src/test/scala/com/evolution/scache/CacheSpec.scala +++ b/src/test/scala/com/evolution/scache/CacheSpec.scala @@ -20,34 +20,45 @@ class CacheSpec extends AsyncFunSuite with Matchers { private val expiringCache = Cache.expiring[IO, Int, Int]( config = ExpiringCache.Config[IO, Int, Int](expireAfterRead = 1.minute), - partitions = None, + partitions = None ) for { (name, cache0) <- List( - ("default" , Cache.loading[IO, Int, Int]), - ("no partitions" , LoadingCache.of(LoadingCache.EntryRefs.empty[IO, Int, Int])), - ("expiring" , expiringCache), - ("expiring no partitions", ExpiringCache.of[IO, Int, Int](ExpiringCache.Config(expireAfterRead = 1.minute)))) + ("default", Cache.loading[IO, Int, Int]), + ( + "no partitions", + LoadingCache.of(LoadingCache.EntryRefs.empty[IO, Int, Int]) + ), + ("expiring", expiringCache), + ( + "expiring no partitions", + ExpiringCache.of[IO, Int, Int]( + ExpiringCache.Config(expireAfterRead = 1.minute) + ) + ) + ) } yield { val cacheAndMetrics = for { - cache <- cache0 + cache <- cache0 metrics <- CacheMetricsProbe.of.toResource - cache <- cache.withMetrics(metrics) - cache <- cache.withFence + cache <- cache.withMetrics(metrics) + cache <- cache.withFence } yield (cache, metrics) - def check(testName: String)(assertions: (Cache[IO, Int, Int], CacheMetricsProbe) => IO[Any]): Unit = test(testName) { + def check(testName: String)( + assertions: (Cache[IO, Int, Int], CacheMetricsProbe) => IO[Any] + ): Unit = test(testName) { cacheAndMetrics.use(assertions.tupled).run(timeout = 20.seconds) } check(s"get: $name") { (cache, metrics) => for { value <- cache.get(0) - _ <- IO { value shouldEqual none[Int] } - _ <- metrics.expect(metrics.expectedGet(hit = false) -> 1) + _ <- IO { value shouldEqual none[Int] } + _ <- metrics.expect(metrics.expectedGet(hit = false) -> 1) } yield {} } @@ -81,10 +92,10 @@ class CacheSpec extends AsyncFunSuite with Matchers { a <- cache.get1(2) _ <- IO { a shouldEqual 2.asRight.some } _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 2, - metrics.expectedGet(hit = true) -> 3, + metrics.expectedGet(hit = false) -> 2, + metrics.expectedGet(hit = true) -> 3, metrics.expectedLoad(success = true) -> 1, - metrics.expectedPut -> 1 + metrics.expectedPut -> 1 ) } yield {} } @@ -92,9 +103,9 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"get succeeds after cache is released: $name") { val result = for { (cache, metrics) <- cacheAndMetrics.use { _.pure[IO] } - a <- cache.get(0) - _ <- IO { a shouldEqual none[Int] } - _ <- metrics.expect(metrics.expectedGet(hit = false) -> 1) + a <- cache.get(0) + _ <- IO { a shouldEqual none[Int] } + _ <- metrics.expect(metrics.expectedGet(hit = false) -> 1) } yield {} result.run() } @@ -102,14 +113,14 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"getOrElse: $name") { (cache, metrics) => for { value <- cache.getOrElse(0, 1.pure[IO]) - _ <- IO { value shouldEqual 1 } - _ <- cache.put(0, 2) + _ <- IO { value shouldEqual 1 } + _ <- cache.put(0, 2) value <- cache.getOrElse(0, 1.pure[IO]) - _ <- IO { value shouldEqual 2 } - _ <- metrics.expect( + _ <- IO { value shouldEqual 2 } + _ <- metrics.expect( metrics.expectedGet(hit = false) -> 1, - metrics.expectedPut -> 1, - metrics.expectedGet(hit = true) -> 1 + metrics.expectedPut -> 1, + metrics.expectedGet(hit = true) -> 1 ) } yield {} } @@ -117,9 +128,9 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"getOrElse succeeds after cache is released: $name") { val result = for { (cache, metrics) <- cacheAndMetrics.use { _.pure[IO] } - a <- cache.getOrElse(0, 1.pure[IO]) - _ <- IO { a shouldEqual 1 } - _ <- metrics.expect(metrics.expectedGet(hit = false) -> 1) + a <- cache.getOrElse(0, 1.pure[IO]) + _ <- IO { a shouldEqual 1 } + _ <- metrics.expect(metrics.expectedGet(hit = false) -> 1) } yield {} result.run() } @@ -128,19 +139,19 @@ class CacheSpec extends AsyncFunSuite with Matchers { for { value <- cache.put(0, 0) value <- value - _ <- IO { value shouldEqual none } + _ <- IO { value shouldEqual none } value <- cache.get(0) - _ <- IO { value shouldEqual 0.some } + _ <- IO { value shouldEqual 0.some } value <- cache.put(0, 1) value <- value - _ <- IO { value shouldEqual 0.some } + _ <- IO { value shouldEqual 0.some } value <- cache.put(0, 2) value <- value - _ <- IO { value shouldEqual 1.some } - _ <- metrics.expect( - metrics.expectedPut -> 3, + _ <- IO { value shouldEqual 1.some } + _ <- metrics.expect( + metrics.expectedPut -> 3, metrics.expectedGet(hit = true) -> 1, - metrics.expectedLife -> 2 + metrics.expectedLife -> 2 ) } yield {} } @@ -148,9 +159,9 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"put succeeds after cache is released: $name") { val result = for { (cache, metrics) <- cacheAndMetrics.use { _.pure[IO] } - a <- cache.put(0, 0).flatten - _ <- IO { a shouldEqual none[Int] } - _ <- metrics.expect(metrics.expectedPut -> 1) + a <- cache.put(0, 0).flatten + _ <- IO { a shouldEqual none[Int] } + _ <- metrics.expect(metrics.expectedPut -> 1) } yield {} result.run() } @@ -160,25 +171,29 @@ class CacheSpec extends AsyncFunSuite with Matchers { release0 <- Deferred[IO, Unit] release1 <- Deferred[IO, Unit] released <- Ref[IO].of(false) - value <- cache.put(0, 0, release0.complete(()) *> release1.get *> released.set(true)) - value <- value - _ <- IO { value shouldEqual none } - value <- cache.get(0) - _ <- IO { value shouldEqual 0.some } - value <- cache.put(0, 1) - _ <- release0.get - _ <- release1.complete(()) - value <- value - _ <- IO { value shouldEqual 0.some } - value <- released.get - _ <- IO { value shouldEqual true } - value <- cache.put(0, 2) - value <- value - _ <- IO { value shouldEqual 1.some } - _ <- metrics.expect( - metrics.expectedPut -> 3, + value <- cache.put( + 0, + 0, + release0.complete(()) *> release1.get *> released.set(true) + ) + value <- value + _ <- IO { value shouldEqual none } + value <- cache.get(0) + _ <- IO { value shouldEqual 0.some } + value <- cache.put(0, 1) + _ <- release0.get + _ <- release1.complete(()) + value <- value + _ <- IO { value shouldEqual 0.some } + value <- released.get + _ <- IO { value shouldEqual true } + value <- cache.put(0, 2) + value <- value + _ <- IO { value shouldEqual 1.some } + _ <- metrics.expect( + metrics.expectedPut -> 3, metrics.expectedGet(hit = true) -> 1, - metrics.expectedLife -> 2 + metrics.expectedLife -> 2 ) } yield {} } @@ -186,9 +201,9 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"put releasable fails after cache is released: $name") { val result = for { (cache, metrics) <- cacheAndMetrics.use { _.pure[IO] } - a <- cache.put(0, 0, ().pure[IO]).flatten.attempt - _ <- IO { a shouldEqual CacheReleasedError.asLeft } - _ <- metrics.expect() + a <- cache.put(0, 0, ().pure[IO]).flatten.attempt + _ <- IO { a shouldEqual CacheReleasedError.asLeft } + _ <- metrics.expect() } yield {} result.run() } @@ -228,9 +243,9 @@ class CacheSpec extends AsyncFunSuite with Matchers { a <- cache.contains(1) _ <- IO { a shouldBe false } _ <- metrics.expect( - metrics.expectedPut -> 2, - metrics.expectedLife -> 2, - metrics.expectedClear -> 1 + metrics.expectedPut -> 2, + metrics.expectedLife -> 2, + metrics.expectedClear -> 1 ) } yield {} } @@ -238,26 +253,26 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"size: $name") { (cache, metrics) => for { size <- cache.size - _ <- IO { size shouldEqual 0 } - _ <- cache.put(0, 0) + _ <- IO { size shouldEqual 0 } + _ <- cache.put(0, 0) size <- cache.size - _ <- IO { size shouldEqual 1 } - _ <- cache.put(0, 1) + _ <- IO { size shouldEqual 1 } + _ <- cache.put(0, 1) size <- cache.size - _ <- IO { size shouldEqual 1 } - _ <- cache.put(1, 1) + _ <- IO { size shouldEqual 1 } + _ <- cache.put(1, 1) size <- cache.size - _ <- IO { size shouldEqual 2 } - _ <- cache.remove(0) + _ <- IO { size shouldEqual 2 } + _ <- cache.remove(0) size <- cache.size - _ <- IO { size shouldEqual 1 } - _ <- cache.clear + _ <- IO { size shouldEqual 1 } + _ <- cache.clear size <- cache.size - _ <- IO { size shouldEqual 0 } - _ <- metrics.expect( - metrics.expectedSize -> 6, - metrics.expectedPut -> 3, - metrics.expectedLife -> 3, + _ <- IO { size shouldEqual 0 } + _ <- metrics.expect( + metrics.expectedSize -> 6, + metrics.expectedPut -> 3, + metrics.expectedLife -> 3, metrics.expectedClear -> 1 ) } yield {} @@ -265,11 +280,13 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"size succeeds after cache is released: $name") { val result = for { - (cache, metrics) <- cacheAndMetrics.use { case (cache, metrics) => cache.put(0, 0).flatten.as((cache, metrics)) } - a <- cache.size - _ <- IO { a shouldEqual 0 } - _ <- metrics.expect( - metrics.expectedPut -> 1, + (cache, metrics) <- cacheAndMetrics.use { case (cache, metrics) => + cache.put(0, 0).flatten.as((cache, metrics)) + } + a <- cache.size + _ <- IO { a shouldEqual 0 } + _ <- metrics.expect( + metrics.expectedPut -> 1, metrics.expectedLife -> 1, metrics.expectedSize -> 1 ) @@ -279,15 +296,15 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"remove: $name") { (cache, metrics) => for { - _ <- cache.put(0, 0) + _ <- cache.put(0, 0) value <- cache.remove(0) value <- value - _ <- IO { value shouldEqual 0.some } + _ <- IO { value shouldEqual 0.some } value <- cache.get(0) - _ <- IO { value shouldEqual none } - _ <- metrics.expect( - metrics.expectedPut -> 1, - metrics.expectedLife -> 1, + _ <- IO { value shouldEqual none } + _ <- metrics.expect( + metrics.expectedPut -> 1, + metrics.expectedLife -> 1, metrics.expectedGet(hit = false) -> 1 ) } yield {} @@ -296,26 +313,26 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"remove succeeds after cache is released: $name") { val result = for { (cache, metrics) <- cacheAndMetrics.use { _.pure[IO] } - a <- cache.remove(0).flatten - _ <- IO { a shouldEqual none } - _ <- metrics.expect() + a <- cache.remove(0).flatten + _ <- IO { a shouldEqual none } + _ <- metrics.expect() } yield {} result.run() } check(s"clear: $name") { (cache, metrics) => for { - _ <- cache.put(0, 0) - _ <- cache.put(1, 1) - _ <- cache.clear + _ <- cache.put(0, 0) + _ <- cache.put(1, 1) + _ <- cache.clear value0 <- cache.get(0) value1 <- cache.get(1) - _ <- IO { value0 shouldEqual none[Int] } - _ <- IO { value1 shouldEqual none[Int] } - _ <- metrics.expect( - metrics.expectedPut -> 2, - metrics.expectedLife -> 2, - metrics.expectedClear -> 1, + _ <- IO { value0 shouldEqual none[Int] } + _ <- IO { value1 shouldEqual none[Int] } + _ <- metrics.expect( + metrics.expectedPut -> 2, + metrics.expectedLife -> 2, + metrics.expectedClear -> 1, metrics.expectedGet(hit = false) -> 2 ) } yield {} @@ -324,51 +341,54 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"clear succeeds after cache is released: $name") { val result = for { (cache, metrics) <- cacheAndMetrics.use { _.pure[IO] } - _ <- cache.clear.flatten - _ <- metrics.expect(metrics.expectedClear -> 1) + _ <- cache.clear.flatten + _ <- metrics.expect(metrics.expectedClear -> 1) } yield {} result.run() } check(s"clear releasable: $name") { (cache, metrics) => for { - release <- Deferred[IO, Unit] + release <- Deferred[IO, Unit] released0 <- Ref[IO].of(false) - _ <- cache.put(0, 0, release.get *> released0.set(true)) - _ <- cache.put(1, 1, TestError.raiseError[IO, Unit]) + _ <- cache.put(0, 0, release.get *> released0.set(true)) + _ <- cache.put(1, 1, TestError.raiseError[IO, Unit]) released1 <- Ref[IO].of(false) - _ <- cache.getOrUpdate1(2) { (2, 2, (release.get *> released1.set(true)).some).pure[IO] } - _ <- cache.getOrUpdate1(3) { (3, 3, TestError.raiseError[IO, Unit].some).pure[IO] } - keys <- cache.keys - _ <- IO { keys shouldEqual Set(0, 1, 2, 3) } - clear <- cache.clear + _ <- cache.getOrUpdate1(2) { + (2, 2, (release.get *> released1.set(true)).some).pure[IO] + } + _ <- cache.getOrUpdate1(3) { + (3, 3, TestError.raiseError[IO, Unit].some).pure[IO] + } + keys <- cache.keys + _ <- IO { keys shouldEqual Set(0, 1, 2, 3) } + clear <- cache.clear _ <- keys.toList.foldMapM { key => for { - value <- cache.get(key) - _ <- IO { value shouldEqual none[Int] } + value <- cache.get(key) + _ <- IO { value shouldEqual none[Int] } } yield {} } - keys <- cache.keys - _ <- IO { keys shouldEqual Set.empty } - _ <- release.complete(()).start - _ <- clear + keys <- cache.keys + _ <- IO { keys shouldEqual Set.empty } + _ <- release.complete(()).start + _ <- clear value <- released0.get - _ <- IO { value shouldEqual true } + _ <- IO { value shouldEqual true } value <- released1.get - _ <- IO { value shouldEqual true } - _ <- metrics.expect( - metrics.expectedKeys -> 2, - metrics.expectedGet(hit = false) -> 6, - metrics.expectedPut -> 2, - metrics.expectedLife -> 4, - metrics.expectedClear -> 1, - metrics.expectedLoad(success = true) -> 2, + _ <- IO { value shouldEqual true } + _ <- metrics.expect( + metrics.expectedKeys -> 2, + metrics.expectedGet(hit = false) -> 6, + metrics.expectedPut -> 2, + metrics.expectedLife -> 4, + metrics.expectedClear -> 1, + metrics.expectedLoad(success = true) -> 2 ) } yield {} } check(s"put & get many: $name") { (cache, metrics) => - val values = (0 to 100).toList def getAll(cache: Cache[IO, Int, Int]) = { @@ -383,14 +403,14 @@ class CacheSpec extends AsyncFunSuite with Matchers { for { result0 <- getAll(cache) - _ <- values.foldMapM { key => cache.put(key, key).void } + _ <- values.foldMapM { key => cache.put(key, key).void } result1 <- getAll(cache) - _ <- IO { result0.flatten.reverse shouldEqual List.empty } - _ <- IO { result1.flatten.reverse shouldEqual values } - _ <- metrics.expect( + _ <- IO { result0.flatten.reverse shouldEqual List.empty } + _ <- IO { result1.flatten.reverse shouldEqual values } + _ <- metrics.expect( metrics.expectedGet(hit = false) -> 101, - metrics.expectedPut -> 101, - metrics.expectedGet(hit = true) -> 101, + metrics.expectedPut -> 101, + metrics.expectedGet(hit = true) -> 101 ) } yield {} } @@ -398,17 +418,17 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"getOrUpdate: $name") { (cache, metrics) => for { deferred <- Deferred[IO, Int] - value0 <- cache.getOrUpdateEnsure(0) { deferred.get } - value2 <- cache.getOrUpdate(0)(1.pure[IO]).startEnsure - _ <- deferred.complete(0) - value0 <- value0.joinWithNever - value1 <- value2.joinWithNever - _ <- IO { value0 shouldEqual 0.asRight } - _ <- IO { value1 shouldEqual 0 } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, + value0 <- cache.getOrUpdateEnsure(0) { deferred.get } + value2 <- cache.getOrUpdate(0)(1.pure[IO]).startEnsure + _ <- deferred.complete(0) + value0 <- value0.joinWithNever + value1 <- value2.joinWithNever + _ <- IO { value0 shouldEqual 0.asRight } + _ <- IO { value1 shouldEqual 0 } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, metrics.expectedLoad(success = true) -> 1, - metrics.expectedGet(hit = true) -> 1 + metrics.expectedGet(hit = true) -> 1 ) } yield {} } @@ -416,11 +436,11 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"getOrUpdate succeeds after cache is released: $name") { val result = for { (cache, metrics) <- cacheAndMetrics.use { _.pure[IO] } - a <- cache.getOrUpdate(0)(1.pure[IO]) - _ <- IO { a shouldEqual 1 } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedLoad(success = true) -> 1, + a <- cache.getOrUpdate(0)(1.pure[IO]) + _ <- IO { a shouldEqual 1 } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, + metrics.expectedLoad(success = true) -> 1 ) } yield {} result.run() @@ -429,54 +449,56 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"getOrUpdateOpt: $name") { (cache, metrics) => for { deferred <- Deferred[IO, Option[Int]] - value0 <- cache.getOrUpdateOptEnsure(0) { deferred.get } - value1 <- cache.getOrUpdateOpt(0)(0.some.pure[IO]).startEnsure - _ <- deferred.complete(none) - value0 <- value0.joinWithNever - value1 <- value1.joinWithNever - _ = value0 shouldEqual none[Int].asRight - _ = value1 shouldEqual none[Int] - value <- cache.getOrUpdateOpt(0)(0.some.pure[IO]) - _ = value shouldEqual 0.some + value0 <- cache.getOrUpdateOptEnsure(0) { deferred.get } + value1 <- cache.getOrUpdateOpt(0)(0.some.pure[IO]).startEnsure + _ <- deferred.complete(none) + value0 <- value0.joinWithNever + value1 <- value1.joinWithNever + _ = value0 shouldEqual none[Int].asRight + _ = value1 shouldEqual none[Int] + value <- cache.getOrUpdateOpt(0)(0.some.pure[IO]) + _ = value shouldEqual 0.some _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 2, - metrics.expectedGet(hit = true) -> 1, - metrics.expectedLoad(success = true) -> 2, + metrics.expectedGet(hit = false) -> 2, + metrics.expectedGet(hit = true) -> 1, + metrics.expectedLoad(success = true) -> 2 ) } yield {} } check(s"getOrUpdate1: $name") { (cache, metrics) => for { - value <- cache.getOrUpdate1(0) { ("a", 0, none[IO[Unit]]).pure[IO] } - _ <- IO { value shouldEqual "a".asLeft } + value <- cache.getOrUpdate1(0) { ("a", 0, none[IO[Unit]]).pure[IO] } + _ <- IO { value shouldEqual "a".asLeft } - value <- cache.getOrUpdate1(0) { ("b", 1, none[IO[Unit]]).pure[IO] } - _ <- IO { value shouldEqual 0.asRight.asRight } + value <- cache.getOrUpdate1(0) { ("b", 1, none[IO[Unit]]).pure[IO] } + _ <- IO { value shouldEqual 0.asRight.asRight } deferred0 <- Deferred[IO, Unit] deferred1 <- Deferred[IO, (String, Int, Option[IO[Unit]])] - fiber <- cache.getOrUpdate1(1) { deferred0.complete(()) *> deferred1.get }.start - _ <- deferred0.get - value <- cache.getOrUpdate1(1) { ("d", 1, none[IO[Unit]]).pure[IO] } - _ <- IO { value should matchPattern { case Right(Left(_)) => } } - released <- Deferred[IO, Unit] - _ <- deferred1.complete(("c", 0, released.complete(()).void.some)) - value <- value.traverse { + fiber <- cache + .getOrUpdate1(1) { deferred0.complete(()) *> deferred1.get } + .start + _ <- deferred0.get + value <- cache.getOrUpdate1(1) { ("d", 1, none[IO[Unit]]).pure[IO] } + _ <- IO { value should matchPattern { case Right(Left(_)) => } } + released <- Deferred[IO, Unit] + _ <- deferred1.complete(("c", 0, released.complete(()).void.some)) + value <- value.traverse { case Right(a) => a.pure[IO] case Left(a) => a } - _ <- IO { value shouldEqual 0.asRight } + _ <- IO { value shouldEqual 0.asRight } value <- fiber.joinWithNever - _ <- IO { value shouldEqual "c".asLeft } - _ <- cache.clear - _ <- released.complete(()) - _ <- metrics.expect( - metrics.expectedGet(hit = true) -> 2, - metrics.expectedGet(hit = false) -> 2, - metrics.expectedLife -> 2, - metrics.expectedClear -> 1, - metrics.expectedLoad(success = true) -> 2, + _ <- IO { value shouldEqual "c".asLeft } + _ <- cache.clear + _ <- released.complete(()) + _ <- metrics.expect( + metrics.expectedGet(hit = true) -> 2, + metrics.expectedGet(hit = false) -> 2, + metrics.expectedLife -> 2, + metrics.expectedClear -> 1, + metrics.expectedLoad(success = true) -> 2 ) } yield {} } @@ -485,22 +507,26 @@ class CacheSpec extends AsyncFunSuite with Matchers { for { deferred0 <- Deferred[IO, Unit] deferred1 <- Deferred[IO, (Int, IO[Unit])] - resource = Resource - .make { deferred0.complete(()) *> deferred1.get } { case (_, release) => release } + resource = Resource + .make { deferred0.complete(()) *> deferred1.get } { + case (_, release) => release + } .map { case (a, _) => a } - fiber0 <- cache.getOrUpdateResource(0) { resource }.start - _ <- deferred0.get - fiber1 <- cache.getOrUpdateResource(0)(1.pure[Resource[IO, *]]).startEnsure - released <- Deferred[IO, Unit] - _ <- deferred1.complete((0, released.get)) - value <- fiber0.joinWithNever - _ <- IO { value shouldEqual 0 } - value <- fiber1.joinWithNever - _ <- IO { value shouldEqual 0 } - _ <- released.complete(()) - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedGet(hit = true) -> 1, + fiber0 <- cache.getOrUpdateResource(0) { resource }.start + _ <- deferred0.get + fiber1 <- cache + .getOrUpdateResource(0)(1.pure[Resource[IO, *]]) + .startEnsure + released <- Deferred[IO, Unit] + _ <- deferred1.complete((0, released.get)) + value <- fiber0.joinWithNever + _ <- IO { value shouldEqual 0 } + value <- fiber1.joinWithNever + _ <- IO { value shouldEqual 0 } + _ <- released.complete(()) + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, + metrics.expectedGet(hit = true) -> 1, metrics.expectedLoad(success = true) -> 1 ) } yield {} @@ -508,32 +534,36 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"getOrUpdateResourceOpt: $name") { (cache, metrics) => for { - deferred <- Deferred[IO, Unit] - resource = Resource + deferred <- Deferred[IO, Unit] + resource = Resource .release { deferred.complete(()).void } .as(none[Int]) - value <- cache.getOrUpdateResourceOpt(0) { resource } - _ <- IO { value shouldEqual none } - _ <- deferred.get + value <- cache.getOrUpdateResourceOpt(0) { resource } + _ <- IO { value shouldEqual none } + _ <- deferred.get deferred0 <- Deferred[IO, Unit] deferred1 <- Deferred[IO, (Option[Int], IO[Unit])] - resource = Resource - .make { deferred0.complete(()) *> deferred1.get } { case (_, release) => release } + resource = Resource + .make { deferred0.complete(()) *> deferred1.get } { + case (_, release) => release + } .map { case (a, _) => a } - fiber0 <- cache.getOrUpdateResourceOpt(0) { resource }.start - _ <- deferred0.get - fiber1 <- cache.getOrUpdateResourceOpt(0)(1.some.pure[Resource[IO, *]]).startEnsure - released <- Deferred[IO, Unit] - _ <- deferred1.complete((0.some, released.get)) - value <- fiber0.joinWithNever - _ <- IO { value shouldEqual 0.some } - value <- fiber1.joinWithNever - _ <- IO { value shouldEqual 0.some } - _ <- released.complete(()) - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 2, + fiber0 <- cache.getOrUpdateResourceOpt(0) { resource }.start + _ <- deferred0.get + fiber1 <- cache + .getOrUpdateResourceOpt(0)(1.some.pure[Resource[IO, *]]) + .startEnsure + released <- Deferred[IO, Unit] + _ <- deferred1.complete((0.some, released.get)) + value <- fiber0.joinWithNever + _ <- IO { value shouldEqual 0.some } + value <- fiber1.joinWithNever + _ <- IO { value shouldEqual 0.some } + _ <- released.complete(()) + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 2, metrics.expectedLoad(success = true) -> 2, - metrics.expectedGet(hit = true) -> 1 + metrics.expectedGet(hit = true) -> 1 ) } yield {} } @@ -541,14 +571,14 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"getOrUpdateOpt1: $name") { (cache, metrics) => for { deferred <- Deferred[IO, Option[(Int, Option[IO[Unit]])]] - value0 <- cache.getOrUpdateOpt1Ensure(0) { deferred.get } - _ <- deferred.complete(none) - value <- value0.joinWithNever - _ <- IO { value shouldEqual none[Int] } - value <- cache.getOrUpdateOpt1(0)((1, 1, none[IO[Unit]]).some.pure[IO]) - _ <- IO { value shouldEqual 1.asLeft.some } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 2, + value0 <- cache.getOrUpdateOpt1Ensure(0) { deferred.get } + _ <- deferred.complete(none) + value <- value0.joinWithNever + _ <- IO { value shouldEqual none[Int] } + value <- cache.getOrUpdateOpt1(0)((1, 1, none[IO[Unit]]).some.pure[IO]) + _ <- IO { value shouldEqual 1.asLeft.some } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 2, metrics.expectedLoad(success = true) -> 2 ) } yield {} @@ -557,11 +587,11 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"getOrUpdate1 does not fail after cache is released: $name") { val result = for { (cache, metrics) <- cacheAndMetrics.use { _.pure[IO] } - a <- cache.getOrUpdate1(0)((1, 1, none[IO[Unit]]).pure[IO]) - _ <- IO { a shouldEqual 1.asLeft } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedLoad(success = true) -> 1, + a <- cache.getOrUpdate1(0)((1, 1, none[IO[Unit]]).pure[IO]) + _ <- IO { a shouldEqual 1.asLeft } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, + metrics.expectedLoad(success = true) -> 1 ) } yield {} result.run() @@ -570,10 +600,10 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"getOrUpdate1 with release fails after cache is released: $name") { val result = for { (cache, metrics) <- cacheAndMetrics.use { _.pure[IO] } - a <- cache.getOrUpdate1(0)((1, 1, IO.unit.some).pure[IO]).attempt - _ <- IO { a shouldEqual CacheReleasedError.asLeft } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, + a <- cache.getOrUpdate1(0)((1, 1, IO.unit.some).pure[IO]).attempt + _ <- IO { a shouldEqual CacheReleasedError.asLeft } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, metrics.expectedLoad(success = false) -> 1 ) } yield {} @@ -583,11 +613,11 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"getOrUpdateOpt1 does not fail after cache is released: $name") { val result = for { (cache, metrics) <- cacheAndMetrics.use { _.pure[IO] } - a <- cache.getOrUpdateOpt1(0)((1, 1, none[IO[Unit]]).some.pure[IO]) - _ <- IO { a shouldEqual 1.asLeft.some } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedLoad(success = true) -> 1, + a <- cache.getOrUpdateOpt1(0)((1, 1, none[IO[Unit]]).some.pure[IO]) + _ <- IO { a shouldEqual 1.asLeft.some } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, + metrics.expectedLoad(success = true) -> 1 ) } yield {} result.run() @@ -596,11 +626,13 @@ class CacheSpec extends AsyncFunSuite with Matchers { test(s"getOrUpdateOpt1 with release fails after cache is released: $name") { val result = for { (cache, metrics) <- cacheAndMetrics.use { _.pure[IO] } - a <- cache.getOrUpdateOpt1(0)((1, 1, IO.unit.some).some.pure[IO]).attempt - _ <- IO { a shouldEqual CacheReleasedError.asLeft } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedLoad(success = false) -> 1, + a <- cache + .getOrUpdateOpt1(0)((1, 1, IO.unit.some).some.pure[IO]) + .attempt + _ <- IO { a shouldEqual CacheReleasedError.asLeft } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, + metrics.expectedLoad(success = false) -> 1 ) } yield {} result.run() @@ -609,22 +641,22 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"cancel getOrUpdate1: $name") { (cache, metrics) => for { deferred0 <- Deferred[IO, (Int, Option[IO[Unit]])] - fiber0 <- cache.getOrUpdate1Ensure(0) { deferred0.get } - fiber1 <- cache.getOrUpdate2(0) { IO.never }.startEnsure - release <- Deferred[IO, Unit] - _ <- fiber0.cancel.start - _ <- deferred0.complete((0, release.complete(()).void.some)) - value <- fiber0.join - _ <- IO { value shouldEqual Outcome.canceled } - value <- fiber1.joinWithNever - _ <- IO { value shouldEqual 0.asRight } - _ <- cache.remove(0) - _ <- release.get - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedGet(hit = true) -> 1, + fiber0 <- cache.getOrUpdate1Ensure(0) { deferred0.get } + fiber1 <- cache.getOrUpdate2(0) { IO.never }.startEnsure + release <- Deferred[IO, Unit] + _ <- fiber0.cancel.start + _ <- deferred0.complete((0, release.complete(()).void.some)) + value <- fiber0.join + _ <- IO { value shouldEqual Outcome.canceled } + value <- fiber1.joinWithNever + _ <- IO { value shouldEqual 0.asRight } + _ <- cache.remove(0) + _ <- release.get + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, + metrics.expectedGet(hit = true) -> 1, metrics.expectedLoad(success = true) -> 1, - metrics.expectedLife -> 1 + metrics.expectedLife -> 1 ) } yield {} } @@ -632,21 +664,21 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"put while getOrUpdate: $name") { (cache, metrics) => for { deferred <- Deferred[IO, Int] - fiber <- cache.getOrUpdateEnsure(0) { deferred.get } - value <- cache.put(0, 1) - _ <- deferred.complete(0) - value <- value - _ <- IO { value shouldEqual none } - value <- fiber.joinWithNever - _ <- IO { value shouldEqual 1.asRight } - value <- cache.get(0) - _ <- IO { value shouldEqual 1.some } - _ <- metrics.expect( - metrics.expectedGet(hit = true) -> 2, + fiber <- cache.getOrUpdateEnsure(0) { deferred.get } + value <- cache.put(0, 1) + _ <- deferred.complete(0) + value <- value + _ <- IO { value shouldEqual none } + value <- fiber.joinWithNever + _ <- IO { value shouldEqual 1.asRight } + value <- cache.get(0) + _ <- IO { value shouldEqual 1.some } + _ <- metrics.expect( + metrics.expectedGet(hit = true) -> 2, metrics.expectedLoad(success = true) -> 1, - metrics.expectedGet(hit = false) -> 1, - metrics.expectedPut -> 1, - metrics.expectedLife -> 1 + metrics.expectedGet(hit = false) -> 1, + metrics.expectedPut -> 1, + metrics.expectedLife -> 1 ) } yield {} } @@ -654,23 +686,23 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"put while getOrUpdate1: $name") { (cache, metrics) => for { deferred <- Deferred[IO, (Int, Option[IO[Unit]])] - fiber <- cache.getOrUpdate1Ensure(0) { deferred.get } - value <- cache.put(0, 1) - release <- Deferred[IO, Unit] - _ <- deferred.complete((0, release.complete(()).void.some)) - _ <- release.get - value <- value - _ <- IO { value shouldEqual none } - value <- fiber.joinWithNever - _ <- IO { value shouldEqual 1.asRight } - value <- cache.get(0) - _ <- IO { value shouldEqual 1.some } - _ <- metrics.expect( - metrics.expectedGet(hit = true) -> 2, + fiber <- cache.getOrUpdate1Ensure(0) { deferred.get } + value <- cache.put(0, 1) + release <- Deferred[IO, Unit] + _ <- deferred.complete((0, release.complete(()).void.some)) + _ <- release.get + value <- value + _ <- IO { value shouldEqual none } + value <- fiber.joinWithNever + _ <- IO { value shouldEqual 1.asRight } + value <- cache.get(0) + _ <- IO { value shouldEqual 1.some } + _ <- metrics.expect( + metrics.expectedGet(hit = true) -> 2, metrics.expectedLoad(success = true) -> 1, - metrics.expectedGet(hit = false) -> 1, - metrics.expectedPut -> 1, - metrics.expectedLife -> 1 + metrics.expectedGet(hit = false) -> 1, + metrics.expectedPut -> 1, + metrics.expectedLife -> 1 ) } yield {} } @@ -680,33 +712,35 @@ class CacheSpec extends AsyncFunSuite with Matchers { fiber <- cache.getOrUpdateEnsure(0) { IO.never[Int] } value <- cache.put(0, 0) value <- value - _ <- IO { value shouldEqual none } + _ <- IO { value shouldEqual none } value <- fiber.joinWithNever - _ <- IO { value shouldEqual 0.asRight } + _ <- IO { value shouldEqual 0.asRight } value <- cache.get(0) - _ <- IO { value shouldEqual 0.some } - _ <- metrics.expect( + _ <- IO { value shouldEqual 0.some } + _ <- metrics.expect( metrics.expectedGet(hit = false) -> 1, - metrics.expectedPut -> 1, - metrics.expectedGet(hit = true) -> 2 + metrics.expectedPut -> 1, + metrics.expectedGet(hit = true) -> 2 ) } yield {} } check(s"put while getOrUpdate1 never: $name") { (cache, metrics) => for { - fiber <- cache.getOrUpdate1Ensure(0) { IO.never[(Int, Option[IO[Unit]])] } + fiber <- cache.getOrUpdate1Ensure(0) { + IO.never[(Int, Option[IO[Unit]])] + } value <- cache.put(0, 0) value <- value - _ <- IO { value shouldEqual none } + _ <- IO { value shouldEqual none } value <- fiber.joinWithNever - _ <- IO { value shouldEqual 0.asRight } + _ <- IO { value shouldEqual 0.asRight } value <- cache.get(0) - _ <- IO { value shouldEqual 0.some } - _ <- metrics.expect( + _ <- IO { value shouldEqual 0.some } + _ <- metrics.expect( metrics.expectedGet(hit = false) -> 1, - metrics.expectedPut -> 1, - metrics.expectedGet(hit = true) -> 2 + metrics.expectedPut -> 1, + metrics.expectedGet(hit = true) -> 2 ) } yield {} } @@ -714,20 +748,20 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"put while getOrUpdate failed: $name") { (cache, metrics) => for { deferred <- Deferred[IO, IO[Int]] - fiber <- cache.getOrUpdateEnsure(0) { deferred.get.flatten } - value <- cache.put(0, 1) - _ <- deferred.complete(TestError.raiseError[IO, Int]) - value <- value - _ <- IO { value shouldEqual none } - value <- fiber.joinWithNever - _ <- IO { value shouldEqual 1.asRight } - value <- cache.get(0) - _ <- IO { value shouldEqual 1.some } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedPut -> 1, + fiber <- cache.getOrUpdateEnsure(0) { deferred.get.flatten } + value <- cache.put(0, 1) + _ <- deferred.complete(TestError.raiseError[IO, Int]) + value <- value + _ <- IO { value shouldEqual none } + value <- fiber.joinWithNever + _ <- IO { value shouldEqual 1.asRight } + value <- cache.get(0) + _ <- IO { value shouldEqual 1.some } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, + metrics.expectedPut -> 1, metrics.expectedLoad(success = false) -> 1, - metrics.expectedGet(hit = true) -> 2 + metrics.expectedGet(hit = true) -> 2 ) } yield {} } @@ -735,20 +769,22 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"put while getOrUpdate1 failed: $name") { (cache, metrics) => for { deferred <- Deferred[IO, IO[(Int, Option[IO[Unit]])]] - fiber <- cache.getOrUpdate1Ensure(0) { deferred.get.flatten } - value <- cache.put(0, 1) - _ <- deferred.complete(TestError.raiseError[IO, (Int, Option[IO[Unit]])]) - value <- value - _ <- IO { value shouldEqual none } - value <- fiber.joinWithNever - _ <- IO { value shouldEqual 1.asRight } - value <- cache.get(0) - _ <- IO { value shouldEqual 1.some } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedPut -> 1, + fiber <- cache.getOrUpdate1Ensure(0) { deferred.get.flatten } + value <- cache.put(0, 1) + _ <- deferred.complete( + TestError.raiseError[IO, (Int, Option[IO[Unit]])] + ) + value <- value + _ <- IO { value shouldEqual none } + value <- fiber.joinWithNever + _ <- IO { value shouldEqual 1.asRight } + value <- cache.get(0) + _ <- IO { value shouldEqual 1.some } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, + metrics.expectedPut -> 1, metrics.expectedLoad(success = false) -> 1, - metrics.expectedGet(hit = true) -> 2 + metrics.expectedGet(hit = true) -> 2 ) } yield {} } @@ -756,17 +792,17 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"get while getOrUpdate: $name") { (cache, metrics) => for { deferred <- Deferred[IO, Int] - value0 <- cache.getOrUpdateEnsure(0) { deferred.get } - value1 <- cache.get(0).startEnsure - _ <- deferred.complete(0) - value <- value0.joinWithNever - _ <- IO { value shouldEqual 0.asRight } - value <- value1.joinWithNever - _ <- IO { value shouldEqual 0.some } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, + value0 <- cache.getOrUpdateEnsure(0) { deferred.get } + value1 <- cache.get(0).startEnsure + _ <- deferred.complete(0) + value <- value0.joinWithNever + _ <- IO { value shouldEqual 0.asRight } + value <- value1.joinWithNever + _ <- IO { value shouldEqual 0.some } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, metrics.expectedLoad(success = true) -> 1, - metrics.expectedGet(hit = true) -> 1 + metrics.expectedGet(hit = true) -> 1 ) } yield {} } @@ -775,18 +811,18 @@ class CacheSpec extends AsyncFunSuite with Matchers { for { deferred <- Deferred[IO, (Int, Option[IO[Unit]])] released <- Deferred[IO, Unit] - value0 <- cache.getOrUpdate1Ensure(0) { deferred.get } - value1 <- cache.get(0).startEnsure - _ <- deferred.complete((0, released.get.some)) - value <- value0.joinWithNever - _ <- IO { value shouldEqual 0.asRight } - value <- value1.joinWithNever - _ <- IO { value shouldEqual 0.some } - _ <- released.complete(()) - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, + value0 <- cache.getOrUpdate1Ensure(0) { deferred.get } + value1 <- cache.get(0).startEnsure + _ <- deferred.complete((0, released.get.some)) + value <- value0.joinWithNever + _ <- IO { value shouldEqual 0.asRight } + value <- value1.joinWithNever + _ <- IO { value shouldEqual 0.some } + _ <- released.complete(()) + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, metrics.expectedLoad(success = true) -> 1, - metrics.expectedGet(hit = true) -> 1 + metrics.expectedGet(hit = true) -> 1 ) } yield {} } @@ -794,15 +830,15 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"get while getOrUpdate failed: $name") { (cache, metrics) => for { deferred <- Deferred[IO, IO[Int]] - value0 <- cache.getOrUpdateEnsure(0) { deferred.get.flatten } - value1 <- cache.get(0).startEnsure - _ <- deferred.complete(TestError.raiseError[IO, Int]) - value <- value0.joinWithNever - _ <- IO { value shouldEqual TestError.asLeft } - value <- value1.joinWithNever - _ <- IO { value shouldEqual none[Int] } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 2, + value0 <- cache.getOrUpdateEnsure(0) { deferred.get.flatten } + value1 <- cache.get(0).startEnsure + _ <- deferred.complete(TestError.raiseError[IO, Int]) + value <- value0.joinWithNever + _ <- IO { value shouldEqual TestError.asLeft } + value <- value1.joinWithNever + _ <- IO { value shouldEqual none[Int] } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 2, metrics.expectedLoad(success = false) -> 1 ) } yield {} @@ -811,15 +847,17 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"get while getOrUpdate1 failed: $name") { (cache, metrics) => for { deferred <- Deferred[IO, IO[(Int, Option[IO[Unit]])]] - value0 <- cache.getOrUpdate1Ensure(0) { deferred.get.flatten } - value1 <- cache.get(0).startEnsure - _ <- deferred.complete(TestError.raiseError[IO, (Int, Option[IO[Unit]])]) - value <- value0.joinWithNever - _ <- IO { value shouldEqual TestError.asLeft } - value <- value1.joinWithNever - _ <- IO { value shouldEqual none[Int] } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 2, + value0 <- cache.getOrUpdate1Ensure(0) { deferred.get.flatten } + value1 <- cache.get(0).startEnsure + _ <- deferred.complete( + TestError.raiseError[IO, (Int, Option[IO[Unit]])] + ) + value <- value0.joinWithNever + _ <- IO { value shouldEqual TestError.asLeft } + value <- value1.joinWithNever + _ <- IO { value shouldEqual none[Int] } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 2, metrics.expectedLoad(success = false) -> 1 ) } yield {} @@ -828,19 +866,19 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"remove while getOrUpdate: $name") { (cache, metrics) => for { deferred <- Deferred[IO, Int] - value0 <- cache.getOrUpdateEnsure(0) { deferred.get } - value1 <- cache.remove(0) - _ <- deferred.complete(0) - value <- value0.joinWithNever - _ <- IO { value shouldEqual 0.asRight } - value <- value1 - _ <- IO { value shouldEqual None } + value0 <- cache.getOrUpdateEnsure(0) { deferred.get } + value1 <- cache.remove(0) + _ <- deferred.complete(0) + value <- value0.joinWithNever + _ <- IO { value shouldEqual 0.asRight } + value <- value1 + _ <- IO { value shouldEqual None } // Value is still present in cache, since calculation ended later than remove - value <- cache.get(0) - _ <- IO { value shouldEqual Some(0) } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedGet(hit = true) -> 1, + value <- cache.get(0) + _ <- IO { value shouldEqual Some(0) } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, + metrics.expectedGet(hit = true) -> 1, metrics.expectedLoad(success = true) -> 1 ) } yield {} @@ -849,25 +887,27 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"remove while getOrUpdate1: $name") { (cache, metrics) => for { deferred <- Deferred[IO, (Int, Option[IO[Unit]])] - fiber <- cache.getOrUpdate1Ensure(0) { deferred.get } - value1 <- cache.remove(0) - release <- Deferred[IO, Unit] + fiber <- cache.getOrUpdate1Ensure(0) { deferred.get } + value1 <- cache.remove(0) + release <- Deferred[IO, Unit] released <- Ref[IO].of(false) - _ <- deferred.complete((0, (release.get *> released.set(true).void).some)) - value <- fiber.joinWithNever - _ <- IO { value shouldEqual 0.asRight } - value <- value1.startEnsure - _ <- release.complete(()) - value <- value.joinWithNever + _ <- deferred.complete( + (0, (release.get *> released.set(true).void).some) + ) + value <- fiber.joinWithNever + _ <- IO { value shouldEqual 0.asRight } + value <- value1.startEnsure + _ <- release.complete(()) + value <- value.joinWithNever released <- released.get - _ <- IO { released shouldEqual false } - _ <- IO { value shouldEqual None } - value <- cache.get(0) + _ <- IO { released shouldEqual false } + _ <- IO { value shouldEqual None } + value <- cache.get(0) // Value is still present in cache, since calculation ended later than remove - _ <- IO { value shouldEqual Some(0) } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedGet(hit = true) -> 1, + _ <- IO { value shouldEqual Some(0) } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, + metrics.expectedGet(hit = true) -> 1, metrics.expectedLoad(success = true) -> 1 ) } yield {} @@ -876,17 +916,17 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"remove while getOrUpdate failed: $name") { (cache, metrics) => for { deferred <- Deferred[IO, IO[Int]] - fiber <- cache.getOrUpdateEnsure(0) { deferred.get.flatten } - value <- cache.remove(0) - _ <- deferred.complete(TestError.raiseError[IO, Int]) - value <- value - _ <- IO { value shouldEqual none } - value <- fiber.joinWithNever - _ <- IO { value shouldEqual TestError.asLeft } - value <- cache.get(0) - _ <- IO { value shouldEqual none } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 2, + fiber <- cache.getOrUpdateEnsure(0) { deferred.get.flatten } + value <- cache.remove(0) + _ <- deferred.complete(TestError.raiseError[IO, Int]) + value <- value + _ <- IO { value shouldEqual none } + value <- fiber.joinWithNever + _ <- IO { value shouldEqual TestError.asLeft } + value <- cache.get(0) + _ <- IO { value shouldEqual none } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 2, metrics.expectedLoad(success = false) -> 1 ) } yield {} @@ -895,17 +935,19 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"remove while getOrUpdate1 failed: $name") { (cache, metrics) => for { deferred <- Deferred[IO, IO[(Int, Option[IO[Unit]])]] - fiber <- cache.getOrUpdate1Ensure(0) { deferred.get.flatten } - value <- cache.remove(0) - _ <- deferred.complete(TestError.raiseError[IO, (Int, Option[IO[Unit]])]) - value <- value - _ <- IO { value shouldEqual none } - value <- fiber.joinWithNever - _ <- IO { value shouldEqual TestError.asLeft } - value <- cache.get(0) - _ <- IO { value shouldEqual none } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 2, + fiber <- cache.getOrUpdate1Ensure(0) { deferred.get.flatten } + value <- cache.remove(0) + _ <- deferred.complete( + TestError.raiseError[IO, (Int, Option[IO[Unit]])] + ) + value <- value + _ <- IO { value shouldEqual none } + value <- fiber.joinWithNever + _ <- IO { value shouldEqual TestError.asLeft } + value <- cache.get(0) + _ <- IO { value shouldEqual none } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 2, metrics.expectedLoad(success = false) -> 1 ) } yield {} @@ -914,22 +956,22 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"clear while getOrUpdate: $name") { (cache, metrics) => for { deferred <- Deferred[IO, Int] - value <- cache.getOrUpdateEnsure(0) { deferred.get } - keys <- cache.keys - _ <- IO { keys shouldEqual Set(0) } - _ <- cache.clear - keys <- cache.keys - _ <- IO { keys shouldEqual Set.empty } - _ <- deferred.complete(0) - value <- value.joinWithNever - _ <- IO { value shouldEqual 0.asRight } - keys <- cache.keys - _ <- IO { keys shouldEqual Set.empty } - _ <- metrics.expect( - metrics.expectedKeys -> 3, - metrics.expectedGet(hit = false) -> 1, - metrics.expectedLife -> 1, - metrics.expectedClear -> 1, + value <- cache.getOrUpdateEnsure(0) { deferred.get } + keys <- cache.keys + _ <- IO { keys shouldEqual Set(0) } + _ <- cache.clear + keys <- cache.keys + _ <- IO { keys shouldEqual Set.empty } + _ <- deferred.complete(0) + value <- value.joinWithNever + _ <- IO { value shouldEqual 0.asRight } + keys <- cache.keys + _ <- IO { keys shouldEqual Set.empty } + _ <- metrics.expect( + metrics.expectedKeys -> 3, + metrics.expectedGet(hit = false) -> 1, + metrics.expectedLife -> 1, + metrics.expectedClear -> 1, metrics.expectedLoad(success = true) -> 1 ) } yield {} @@ -937,26 +979,28 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"clear while getOrUpdate1: $name") { (cache, metrics) => for { - release <- Deferred[IO, Unit] + release <- Deferred[IO, Unit] released <- Ref[IO].of(false) - value <- cache.getOrUpdate1(0) { (0, 0, (release.get *> released.set(true)).some).pure[IO] } - _ <- IO { value shouldEqual 0.asLeft } - keys <- cache.keys - _ <- IO { keys shouldEqual Set(0) } - clear <- cache.clear - keys <- cache.keys - _ <- IO { keys shouldEqual Set.empty } - _ <- release.complete(()) - _ <- clear + value <- cache.getOrUpdate1(0) { + (0, 0, (release.get *> released.set(true)).some).pure[IO] + } + _ <- IO { value shouldEqual 0.asLeft } + keys <- cache.keys + _ <- IO { keys shouldEqual Set(0) } + clear <- cache.clear + keys <- cache.keys + _ <- IO { keys shouldEqual Set.empty } + _ <- release.complete(()) + _ <- clear released <- released.get - _ <- IO { released shouldEqual true } - keys <- cache.keys - _ <- IO { keys shouldEqual Set.empty } + _ <- IO { released shouldEqual true } + keys <- cache.keys + _ <- IO { keys shouldEqual Set.empty } _ <- metrics.expect( - metrics.expectedKeys -> 3, - metrics.expectedGet(hit = false) -> 1, - metrics.expectedLife -> 1, - metrics.expectedClear -> 1, + metrics.expectedKeys -> 3, + metrics.expectedGet(hit = false) -> 1, + metrics.expectedLife -> 1, + metrics.expectedClear -> 1, metrics.expectedLoad(success = true) -> 1 ) } yield {} @@ -965,28 +1009,28 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"clear while getOrUpdate1 loading: $name") { (cache, metrics) => for { deferred <- Deferred[IO, (Int, Option[IO[Unit]])] - value <- cache.getOrUpdate1Ensure(0) { deferred.get } - keys <- cache.keys - _ <- IO { keys shouldEqual Set(0) } - clear <- cache.clear - keys <- cache.keys - _ <- IO { keys shouldEqual Set.empty } - release <- Deferred[IO, Unit] + value <- cache.getOrUpdate1Ensure(0) { deferred.get } + keys <- cache.keys + _ <- IO { keys shouldEqual Set(0) } + clear <- cache.clear + keys <- cache.keys + _ <- IO { keys shouldEqual Set.empty } + release <- Deferred[IO, Unit] released <- Ref[IO].of(false) - _ <- deferred.complete((0, (release.get *> released.set(true)).some)) - value <- value.joinWithNever - _ <- IO { value shouldEqual 0.asRight } - keys <- cache.keys - _ <- IO { keys shouldEqual Set.empty } - _ <- release.complete(()) - _ <- clear + _ <- deferred.complete((0, (release.get *> released.set(true)).some)) + value <- value.joinWithNever + _ <- IO { value shouldEqual 0.asRight } + keys <- cache.keys + _ <- IO { keys shouldEqual Set.empty } + _ <- release.complete(()) + _ <- clear released <- released.get - _ <- IO { released shouldEqual true } + _ <- IO { released shouldEqual true } _ <- metrics.expect( - metrics.expectedKeys -> 3, - metrics.expectedGet(hit = false) -> 1, - metrics.expectedLife -> 1, - metrics.expectedClear -> 1, + metrics.expectedKeys -> 3, + metrics.expectedGet(hit = false) -> 1, + metrics.expectedLife -> 1, + metrics.expectedClear -> 1, metrics.expectedLoad(success = true) -> 1 ) } yield {} @@ -994,46 +1038,46 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"keys: $name") { (cache, metrics) => for { - _ <- cache.put(0, 0) + _ <- cache.put(0, 0) keys <- cache.keys - _ <- IO { keys shouldEqual Set(0) } - _ <- cache.put(1, 1) + _ <- IO { keys shouldEqual Set(0) } + _ <- cache.put(1, 1) keys <- cache.keys - _ <- IO { keys shouldEqual Set(0, 1) } - _ <- cache.put(2, 2) + _ <- IO { keys shouldEqual Set(0, 1) } + _ <- cache.put(2, 2) keys <- cache.keys - _ <- IO { keys shouldEqual Set(0, 1, 2) } - _ <- cache.clear + _ <- IO { keys shouldEqual Set(0, 1, 2) } + _ <- cache.clear keys <- cache.keys - _ <- IO { keys shouldEqual Set.empty } - _ <- metrics.expect( - metrics.expectedPut -> 3, - metrics.expectedKeys -> 4, + _ <- IO { keys shouldEqual Set.empty } + _ <- metrics.expect( + metrics.expectedPut -> 3, + metrics.expectedKeys -> 4, metrics.expectedClear -> 1, - metrics.expectedLife -> 3 + metrics.expectedLife -> 3 ) } yield {} } check(s"values: $name") { (cache, metrics) => for { - _ <- cache.put(0, 0) + _ <- cache.put(0, 0) values <- cache.valuesFlatten - _ <- IO { values shouldEqual Map((0, 0)) } - _ <- cache.put(1, 1) + _ <- IO { values shouldEqual Map((0, 0)) } + _ <- cache.put(1, 1) values <- cache.valuesFlatten - _ <- IO { values shouldEqual Map((0, 0), (1, 1)) } - _ <- cache.put(2, 2) + _ <- IO { values shouldEqual Map((0, 0), (1, 1)) } + _ <- cache.put(2, 2) values <- cache.valuesFlatten - _ <- IO { values shouldEqual Map((0, 0), (1, 1), (2, 2)) } - _ <- cache.clear + _ <- IO { values shouldEqual Map((0, 0), (1, 1), (2, 2)) } + _ <- cache.clear values <- cache.valuesFlatten - _ <- IO { values shouldEqual Map.empty } - _ <- metrics.expect( - metrics.expectedPut -> 3, + _ <- IO { values shouldEqual Map.empty } + _ <- metrics.expect( + metrics.expectedPut -> 3, metrics.expectedValues -> 4, - metrics.expectedClear -> 1, - metrics.expectedLife -> 3 + metrics.expectedClear -> 1, + metrics.expectedLife -> 3 ) } yield {} } @@ -1050,39 +1094,53 @@ class CacheSpec extends AsyncFunSuite with Matchers { d <- Deferred[IO, Int] b <- cache.getOrUpdateEnsure(3) { d.get } a <- cache.values1 - _ <- IO { a.map { case (k, v) => (k, v.toOption) } shouldEqual Map((0, 0.some), (1, 1.some), (2, 2.some), (3, none)) } + _ <- IO { + a.map { case (k, v) => (k, v.toOption) } shouldEqual Map( + (0, 0.some), + (1, 1.some), + (2, 2.some), + (3, none) + ) + } _ <- d.complete(3) _ <- b.join a <- cache.values1 - _ <- IO { a shouldEqual Map((0, 0.asRight), (1, 1.asRight), (2, 2.asRight), (3, 3.asRight)) } + _ <- IO { + a shouldEqual Map( + (0, 0.asRight), + (1, 1.asRight), + (2, 2.asRight), + (3, 3.asRight) + ) + } _ <- cache.clear a <- cache.values1 _ <- IO { a shouldEqual Map.empty } _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedPut -> 3, - metrics.expectedClear -> 1, + metrics.expectedGet(hit = false) -> 1, + metrics.expectedPut -> 3, + metrics.expectedClear -> 1, metrics.expectedLoad(success = true) -> 1, - metrics.expectedValues -> 5, - metrics.expectedLife -> 4 + metrics.expectedValues -> 5, + metrics.expectedLife -> 4 ) } yield {} } check(s"cancellation: $name") { (cache, metrics) => for { - deferred <- Deferred[IO, Int] - fiber <- cache.getOrUpdateEnsure(0) { deferred.get } - _ <- fiber.cancel.start - _ <- deferred.complete(0) + deferred <- Deferred[IO, Int] + fiber <- cache.getOrUpdateEnsure(0) { deferred.get } + _ <- fiber.cancel.start + _ <- deferred.complete(0) cancelOutcome <- fiber.join - _ <- IO { cancelOutcome shouldEqual Outcome.canceled } - value <- cache.get(0) - _ <- IO { value shouldEqual 0.some } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, + _ <- IO { cancelOutcome shouldEqual Outcome.canceled } + value <- cache.get(0) + _ <- IO { value shouldEqual 0.some } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 1, metrics.expectedLoad(success = true) -> 1, - metrics.expectedGet(hit = true) -> 1 + metrics.expectedGet(hit = true) -> 1 ) } yield {} } @@ -1092,13 +1150,13 @@ class CacheSpec extends AsyncFunSuite with Matchers { .use { case (cache, metrics) => for { deferred <- Deferred[IO, Int] - fiber <- cache.getOrUpdateEnsure(0)(deferred.get) - fiber <- fiber.cancel.start - result <- cache.get(0) - _ <- IO { result shouldEqual none } - _ <- deferred.complete(0) - _ <- fiber.joinWithNever - _ <- metrics.expect(metrics.expectedGet(hit = false) -> 2) + fiber <- cache.getOrUpdateEnsure(0)(deferred.get) + fiber <- fiber.cancel.start + result <- cache.get(0) + _ <- IO { result shouldEqual none } + _ <- deferred.complete(0) + _ <- fiber.joinWithNever + _ <- metrics.expect(metrics.expectedGet(hit = false) -> 2) } yield {} } .run() @@ -1107,16 +1165,16 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"no leak in case of failure: $name") { (cache, metrics) => for { result <- cache.getOrUpdate(0)(TestError.raiseError[IO, Int]).attempt - _ <- IO { result shouldEqual TestError.asLeft[Int] } + _ <- IO { result shouldEqual TestError.asLeft[Int] } result <- cache.getOrUpdate(0)(0.pure[IO]).attempt - _ <- IO { result shouldEqual 0.asRight } + _ <- IO { result shouldEqual 0.asRight } result <- cache.getOrUpdate(0)(TestError.raiseError[IO, Int]).attempt - _ <- IO { result shouldEqual 0.asRight } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 2, + _ <- IO { result shouldEqual 0.asRight } + _ <- metrics.expect( + metrics.expectedGet(hit = false) -> 2, metrics.expectedLoad(success = false) -> 1, - metrics.expectedLoad(success = true) -> 1, - metrics.expectedGet(hit = true) -> 1 + metrics.expectedLoad(success = true) -> 1, + metrics.expectedGet(hit = true) -> 1 ) } yield {} } @@ -1124,14 +1182,15 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"foldMap: $name") { (cache, metrics) => for { _ <- cache.put(0, 1) - expect = (a: Int, i: Int) => for { - b <- cache.foldMap { case (_, b) => b.map { _.pure[IO] }.merge } - _ <- IO { b shouldEqual a } - _ <- metrics.expect( - metrics.expectedPut -> i, - metrics.expectedFoldMap -> i, - ) - } yield {} + expect = (a: Int, i: Int) => + for { + b <- cache.foldMap { case (_, b) => b.map { _.pure[IO] }.merge } + _ <- IO { b shouldEqual a } + _ <- metrics.expect( + metrics.expectedPut -> i, + metrics.expectedFoldMap -> i + ) + } yield {} _ <- expect(1, 1) _ <- cache.put(1, 2) _ <- expect(3, 2) @@ -1149,9 +1208,9 @@ class CacheSpec extends AsyncFunSuite with Matchers { _ <- cache.getOrUpdateEnsure(0) { d0.complete(()).productR(d1.get) } _ <- cache.getOrUpdateEnsure(1) { d2.complete(()).productR(d3.get) } _ <- cache.getOrUpdateEnsure(2) { d4.complete(()).productR(d5.get) } - fiber <- cache - .foldMapPar { case (_, b) => b.map { _.pure[IO] }.merge } - .start + fiber <- cache.foldMapPar { case (_, b) => + b.map { _.pure[IO] }.merge + }.start _ <- d0.get _ <- d2.get _ <- d4.get @@ -1161,14 +1220,16 @@ class CacheSpec extends AsyncFunSuite with Matchers { a <- fiber.join _ <- IO { a shouldEqual 6.pure[Outcome[IO, Throwable, *]] } _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 3, + metrics.expectedGet(hit = false) -> 3, metrics.expectedLoad(success = true) -> 3, - metrics.expectedFoldMap -> 1 + metrics.expectedFoldMap -> 1 ) } yield {} } - check(s"each release performed exactly once during `getOrUpdate1` and `put` race: $name") { (cache, _) => + check( + s"each release performed exactly once during `getOrUpdate1` and `put` race: $name" + ) { (cache, _) => for { resultRef1 <- Ref[IO].of(0) resultRef2 <- Ref[IO].of(0) @@ -1177,12 +1238,19 @@ class CacheSpec extends AsyncFunSuite with Matchers { // For `getOrUpdate*` we don't know how many times the resource will be run, // so we use increment/decrement as a way to check that the resource is released exactly once. - valueResource = (i: Int) => Resource.make(resultRef1.update(_ + i).as(i))(_ => resultRef1.update(_ - i)) - f1 <- range.parTraverse { i => cache.getOrUpdateResource(0)(valueResource(i)) }.start + valueResource = (i: Int) => + Resource.make(resultRef1.update(_ + i).as(i))(_ => + resultRef1.update(_ - i) + ) + f1 <- range.parTraverse { i => + cache.getOrUpdateResource(0)(valueResource(i)) + }.start // For `put` we know that the resource will be written and released every time, // so we increment on release and check that the final value is equal to the sum of the range. - f2 <- range.parTraverse(i => cache.put(0, 0, resultRef2.update(_ + i))).start + f2 <- range + .parTraverse(i => cache.put(0, 0, resultRef2.update(_ + i))) + .start expectedResult = range.sum @@ -1197,13 +1265,17 @@ class CacheSpec extends AsyncFunSuite with Matchers { } yield {} } - check(s"each release performed exactly once during `put` and `remove` race: $name") { (cache, _) => + check( + s"each release performed exactly once during `put` and `remove` race: $name" + ) { (cache, _) => for { resultRef <- Ref[IO].of(0) n = 100000 range = (1 to n).toList - f1 <- range.parTraverse(i => cache.put(0, 0, resultRef.update(_ + i))).start + f1 <- range + .parTraverse(i => cache.put(0, 0, resultRef.update(_ + i))) + .start f2 <- cache.remove(0).replicateA(n).start expectedResult = range.sum @@ -1217,10 +1289,13 @@ class CacheSpec extends AsyncFunSuite with Matchers { } yield {} } - check(s"each release performed exactly once during " + - s"`getOrUpdate1`, `put`, `modify` and `remove` race: $name") { (cache, _) => - - def modify(releaseCounter: Ref[IO, Int]): Option[Int] => (Int, Directive[IO, Int]) = { + check( + s"each release performed exactly once during " + + s"`getOrUpdate1`, `put`, `modify` and `remove` race: $name" + ) { (cache, _) => + def modify( + releaseCounter: Ref[IO, Int] + ): Option[Int] => (Int, Directive[IO, Int]) = { case Some(i) => i -> Directive.Put(i, releaseCounter.update(_ + 1).some) case None => -2 -> Directive.Put(-2, releaseCounter.update(_ + 1).some) } @@ -1234,12 +1309,19 @@ class CacheSpec extends AsyncFunSuite with Matchers { // For `getOrUpdate*` we don't know how many times the resource will be run, // so we use increment/decrement as a way to check that the resource is released exactly once. - valueResource = (i: Int) => Resource.make(resultRef1.update(_ + i).as(i))(_ => resultRef1.update(_ - i)) - f1 <- range.parTraverse { i => cache.getOrUpdateResource(0)(valueResource(i)) }.start + valueResource = (i: Int) => + Resource.make(resultRef1.update(_ + i).as(i))(_ => + resultRef1.update(_ - i) + ) + f1 <- range.parTraverse { i => + cache.getOrUpdateResource(0)(valueResource(i)) + }.start // For `put` we know that the resource will be written and released every time, // so we increment on release and check that the final value is equal to the sum of the range. - f2 <- range.parTraverse(i => cache.put(0, 0, resultRef2.update(_ + i))).start + f2 <- range + .parTraverse(i => cache.put(0, 0, resultRef2.update(_ + i))) + .start f3 <- range.parTraverse(_ => cache.modify(0)(modify(resultRef3))).start @@ -1262,10 +1344,13 @@ class CacheSpec extends AsyncFunSuite with Matchers { } yield {} } - check(s"failing loads don't interfere with releases during " + - s"`getOrUpdate1`, `put`, `modify` and `remove` race: $name") { (cache, _) => - - def modify(releaseCounter: Ref[IO, Int]): Option[Int] => (Int, Directive[IO, Int]) = { + check( + s"failing loads don't interfere with releases during " + + s"`getOrUpdate1`, `put`, `modify` and `remove` race: $name" + ) { (cache, _) => + def modify( + releaseCounter: Ref[IO, Int] + ): Option[Int] => (Int, Directive[IO, Int]) = { case Some(i) => i -> Directive.Put(i, releaseCounter.update(_ + 1).some) case None => -2 -> Directive.Put(-2, releaseCounter.update(_ + 1).some) } @@ -1280,20 +1365,31 @@ class CacheSpec extends AsyncFunSuite with Matchers { // For `getOrUpdate*` we don't know how many times the resource will be run, // so we use increment/decrement as a way to check that the resource is released exactly once. - valueResource = (i: Int) => Resource.make(resultRef1.update(_ + i).as(i))(_ => resultRef1.update(_ - i)) + valueResource = (i: Int) => + Resource.make(resultRef1.update(_ + i).as(i))(_ => + resultRef1.update(_ - i) + ) f1 <- range.parTraverse { i => - cache.getOrUpdateResource(0)(valueResource(i)).recover { case _ => -1 } + cache.getOrUpdateResource(0)(valueResource(i)).recover { case _ => + -1 + } }.start failingResource = (i: Int) => - Resource.make(new Exception("Boom").raiseError[IO, Int])(_ => resultRef2.update(_ - i)) + Resource.make(new Exception("Boom").raiseError[IO, Int])(_ => + resultRef2.update(_ - i) + ) f2 <- range.parTraverse { i => - cache.getOrUpdateResource(0)(failingResource(i)).recover { case _ => -1 } + cache.getOrUpdateResource(0)(failingResource(i)).recover { case _ => + -1 + } }.start // For `put` we know that the resource will be written and released every time, // so we increment on release and check that the final value is equal to the sum of the range. - f3 <- range.parTraverse(i => cache.put(0, 0, resultRef3.update(_ + i))).start + f3 <- range + .parTraverse(i => cache.put(0, 0, resultRef3.update(_ + i))) + .start f4 <- range.parTraverse(_ => cache.modify(0)(modify(resultRef4))).start @@ -1322,7 +1418,7 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"modify modifies existing entry: $name") { (cache, metrics) => val modify: Option[Int] => (Int, Directive[IO, Int]) = { case Some(i) => i -> Directive.Put(i + 1, None) - case None => -1 -> Directive.Ignore + case None => -1 -> Directive.Ignore } for { (a, release1) <- cache.modify(0)(modify) @@ -1338,16 +1434,23 @@ class CacheSpec extends AsyncFunSuite with Matchers { _ <- metrics.expect( metrics.expectedPut -> 1, - metrics.expectedModify(entryExisted = false, CacheMetrics.Directive.Ignore) -> 1, - metrics.expectedModify(entryExisted = true, CacheMetrics.Directive.Put) -> 1, + metrics.expectedModify( + entryExisted = false, + CacheMetrics.Directive.Ignore + ) -> 1, + metrics.expectedModify( + entryExisted = true, + CacheMetrics.Directive.Put + ) -> 1, metrics.expectedGet(true) -> 1, - metrics.expectedLife -> 2, + metrics.expectedLife -> 2 ) } yield () } check(s"modify keeps existing entry: $name") { (cache, metrics) => - val modify: Option[Int] => (Int, Directive[IO, Int]) = i => i.getOrElse(-1) -> Directive.Ignore + val modify: Option[Int] => (Int, Directive[IO, Int]) = + i => i.getOrElse(-1) -> Directive.Ignore for { (a, release1) <- cache.modify(0)(modify) _ <- IO { a shouldEqual -1 } @@ -1359,9 +1462,15 @@ class CacheSpec extends AsyncFunSuite with Matchers { _ <- List(release1, release2).flatten.sequence_ _ <- metrics.expect( metrics.expectedPut -> 1, - metrics.expectedModify(entryExisted = false, CacheMetrics.Directive.Ignore) -> 1, - metrics.expectedModify(entryExisted = true, CacheMetrics.Directive.Ignore) -> 1, - metrics.expectedGet(true) -> 1, + metrics.expectedModify( + entryExisted = false, + CacheMetrics.Directive.Ignore + ) -> 1, + metrics.expectedModify( + entryExisted = true, + CacheMetrics.Directive.Ignore + ) -> 1, + metrics.expectedGet(true) -> 1 ) } yield () } @@ -1369,7 +1478,7 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"modify removes existing entry: $name") { (cache, metrics) => val modify: Option[Int] => (Int, Directive[IO, Int]) = { case Some(i) => i -> Directive.Remove - case None => -1 -> Directive.Ignore + case None => -1 -> Directive.Ignore } for { (a, release1) <- cache.modify(0)(modify) @@ -1382,10 +1491,16 @@ class CacheSpec extends AsyncFunSuite with Matchers { _ <- List(release1, release2).flatten.sequence_ _ <- metrics.expect( metrics.expectedPut -> 1, - metrics.expectedModify(entryExisted = false, CacheMetrics.Directive.Ignore) -> 1, - metrics.expectedModify(entryExisted = true, CacheMetrics.Directive.Remove) -> 1, + metrics.expectedModify( + entryExisted = false, + CacheMetrics.Directive.Ignore + ) -> 1, + metrics.expectedModify( + entryExisted = true, + CacheMetrics.Directive.Remove + ) -> 1, metrics.expectedGet(false) -> 1, - metrics.expectedLife -> 1, + metrics.expectedLife -> 1 ) } yield () } @@ -1393,7 +1508,7 @@ class CacheSpec extends AsyncFunSuite with Matchers { check(s"modify adds entry when absent: $name") { (cache, metrics) => val modify: Option[Int] => (Int, Directive[IO, Int]) = { case Some(i) => i -> Directive.Ignore - case None => 1 -> Directive.Put(1, None) + case None => 1 -> Directive.Put(1, None) } for { (a, release1) <- cache.modify(0)(modify) @@ -1404,131 +1519,196 @@ class CacheSpec extends AsyncFunSuite with Matchers { _ <- IO { value shouldBe Some(1) } _ <- List(release1, release2).flatten.sequence_ _ <- metrics.expect( - metrics.expectedModify(entryExisted = false, CacheMetrics.Directive.Put) -> 1, - metrics.expectedModify(entryExisted = true, CacheMetrics.Directive.Ignore) -> 1, - metrics.expectedGet(true) -> 1, + metrics.expectedModify( + entryExisted = false, + CacheMetrics.Directive.Put + ) -> 1, + metrics.expectedModify( + entryExisted = true, + CacheMetrics.Directive.Ignore + ) -> 1, + metrics.expectedGet(true) -> 1 ) } yield () } - check(s"modify guarantees updated value write concurrently accessing single key: $name") { - (cache, metrics) => - def modify(releaseCounter: Ref[IO, Int]): Option[Int] => (Int, Directive[IO, Int]) = { - case Some(i) => i -> Directive.Put(i + 1, releaseCounter.update(_ + i + 1).some) - case None => 0 -> Directive.Put(1, releaseCounter.update(_ + 1).some) - } - for { - releaseCounter <- Ref[IO].of(0) - n = 100000 - range = (1 to n).toList + check( + s"modify guarantees updated value write concurrently accessing single key: $name" + ) { (cache, metrics) => + def modify( + releaseCounter: Ref[IO, Int] + ): Option[Int] => (Int, Directive[IO, Int]) = { + case Some(i) => + i -> Directive.Put(i + 1, releaseCounter.update(_ + i + 1).some) + case None => 0 -> Directive.Put(1, releaseCounter.update(_ + 1).some) + } + for { + releaseCounter <- Ref[IO].of(0) + n = 100000 + range = (1 to n).toList - f1 <- range.parTraverse(_ => cache.modify(0)(modify(releaseCounter))).start + f1 <- range + .parTraverse(_ => cache.modify(0)(modify(releaseCounter))) + .start - expectedResult = range.sum + expectedResult = range.sum - results <- f1.joinWithNever - _ <- IO { results.map(_._1).sum shouldEqual (expectedResult - n) } + results <- f1.joinWithNever + _ <- IO { results.map(_._1).sum shouldEqual (expectedResult - n) } - // Waiting for releases - _ <- results.flatMap(_._2).sequence_ + // Waiting for releases + _ <- results.flatMap(_._2).sequence_ - lastWrittenValue <- cache.get(0) - (lastValueRemoved, lastRelease) <- cache.modify(0)(lastValue => (lastValue, Directive.Remove)) - _ <- lastRelease.sequence_ - releasedValuesSum <- releaseCounter.get + lastWrittenValue <- cache.get(0) + (lastValueRemoved, lastRelease) <- cache.modify(0)(lastValue => + (lastValue, Directive.Remove) + ) + _ <- lastRelease.sequence_ + releasedValuesSum <- releaseCounter.get - _ <- IO { releasedValuesSum shouldEqual expectedResult } - _ <- IO { lastWrittenValue shouldBe n.some } - _ <- IO { lastValueRemoved shouldBe n.some } + _ <- IO { releasedValuesSum shouldEqual expectedResult } + _ <- IO { lastWrittenValue shouldBe n.some } + _ <- IO { lastValueRemoved shouldBe n.some } - _ <- metrics.expect( - metrics.expectedModify(entryExisted = false, CacheMetrics.Directive.Put) -> 1, - metrics.expectedModify(entryExisted = true, CacheMetrics.Directive.Put) -> (n - 1), - metrics.expectedModify(entryExisted = true, CacheMetrics.Directive.Remove) -> 1, - metrics.expectedGet(true) -> 1, - metrics.expectedLife -> n, - ) - } yield () + _ <- metrics.expect( + metrics.expectedModify( + entryExisted = false, + CacheMetrics.Directive.Put + ) -> 1, + metrics.expectedModify( + entryExisted = true, + CacheMetrics.Directive.Put + ) -> (n - 1), + metrics.expectedModify( + entryExisted = true, + CacheMetrics.Directive.Remove + ) -> 1, + metrics.expectedGet(true) -> 1, + metrics.expectedLife -> n + ) + } yield () } - check(s"modify guarantees updated value write concurrently accessing multiple keys: $name") { - (cache, metrics) => - def modify(releaseCounter: Ref[IO, Int]): Option[Int] => (Int, Directive[IO, Int]) = { - case Some(i) => i -> Directive.Put(i + 1, releaseCounter.update(_ + i + 1).some) - case None => 0 -> Directive.Put(1, releaseCounter.update(_ + 1).some) - } - for { - releaseCounter <- Ref[IO].of(0) - n = 100000 - range = (1 to n).toList - - f0 <- range.parTraverse(_ => cache.modify(0)(modify(releaseCounter))).start - f1 <- range.parTraverse(_ => cache.modify(1)(modify(releaseCounter))).start - f2 <- range.parTraverse(_ => cache.modify(2)(modify(releaseCounter))).start - f3 <- range.parTraverse(_ => cache.modify(3)(modify(releaseCounter))).start - - expectedResult = range.sum - - results <- List(f0, f1, f2, f3).flatTraverse(_.joinWithNever) - _ <- IO { results.map(_._1).sum shouldEqual (expectedResult - n) * 4 } - - // Waiting for releases - _ <- results.flatMap(_._2).sequence_ - - lastWrittenValue0 <- cache.get(0) - lastWrittenValue1 <- cache.get(1) - lastWrittenValue2 <- cache.get(2) - lastWrittenValue3 <- cache.get(3) - - (lastValueRemoved0, lastRelease0) <- cache.modify(0)(lastValue => (lastValue, Directive.Remove)) - (lastValueRemoved1, lastRelease1) <- cache.modify(1)(lastValue => (lastValue, Directive.Remove)) - (lastValueRemoved2, lastRelease2) <- cache.modify(2)(lastValue => (lastValue, Directive.Remove)) - (lastValueRemoved3, lastRelease3) <- cache.modify(3)(lastValue => (lastValue, Directive.Remove)) - _ <- List(lastRelease0, lastRelease1, lastRelease2, lastRelease3).flatten.sequence_ - - releasedValuesSum <- releaseCounter.get - - _ <- IO { releasedValuesSum shouldEqual expectedResult * 4 } - - _ <- IO { lastWrittenValue0 shouldBe n.some } - _ <- IO { lastWrittenValue1 shouldBe n.some } - _ <- IO { lastWrittenValue2 shouldBe n.some } - _ <- IO { lastWrittenValue3 shouldBe n.some } - - _ <- IO { lastValueRemoved0 shouldBe n.some } - _ <- IO { lastValueRemoved1 shouldBe n.some } - _ <- IO { lastValueRemoved2 shouldBe n.some } - _ <- IO { lastValueRemoved3 shouldBe n.some } - - _ <- metrics.expect( - metrics.expectedModify(entryExisted = false, CacheMetrics.Directive.Put) -> 4, - metrics.expectedModify(entryExisted = true, CacheMetrics.Directive.Put) -> (n - 1) * 4, - metrics.expectedModify(entryExisted = true, CacheMetrics.Directive.Remove) -> 4, - metrics.expectedGet(true) -> 4, - metrics.expectedLife -> n * 4, - ) - } yield () + check( + s"modify guarantees updated value write concurrently accessing multiple keys: $name" + ) { (cache, metrics) => + def modify( + releaseCounter: Ref[IO, Int] + ): Option[Int] => (Int, Directive[IO, Int]) = { + case Some(i) => + i -> Directive.Put(i + 1, releaseCounter.update(_ + i + 1).some) + case None => 0 -> Directive.Put(1, releaseCounter.update(_ + 1).some) + } + for { + releaseCounter <- Ref[IO].of(0) + n = 100000 + range = (1 to n).toList + + f0 <- range + .parTraverse(_ => cache.modify(0)(modify(releaseCounter))) + .start + f1 <- range + .parTraverse(_ => cache.modify(1)(modify(releaseCounter))) + .start + f2 <- range + .parTraverse(_ => cache.modify(2)(modify(releaseCounter))) + .start + f3 <- range + .parTraverse(_ => cache.modify(3)(modify(releaseCounter))) + .start + + expectedResult = range.sum + + results <- List(f0, f1, f2, f3).flatTraverse(_.joinWithNever) + _ <- IO { results.map(_._1).sum shouldEqual (expectedResult - n) * 4 } + + // Waiting for releases + _ <- results.flatMap(_._2).sequence_ + + lastWrittenValue0 <- cache.get(0) + lastWrittenValue1 <- cache.get(1) + lastWrittenValue2 <- cache.get(2) + lastWrittenValue3 <- cache.get(3) + + (lastValueRemoved0, lastRelease0) <- cache.modify(0)(lastValue => + (lastValue, Directive.Remove) + ) + (lastValueRemoved1, lastRelease1) <- cache.modify(1)(lastValue => + (lastValue, Directive.Remove) + ) + (lastValueRemoved2, lastRelease2) <- cache.modify(2)(lastValue => + (lastValue, Directive.Remove) + ) + (lastValueRemoved3, lastRelease3) <- cache.modify(3)(lastValue => + (lastValue, Directive.Remove) + ) + _ <- List( + lastRelease0, + lastRelease1, + lastRelease2, + lastRelease3 + ).flatten.sequence_ + + releasedValuesSum <- releaseCounter.get + + _ <- IO { releasedValuesSum shouldEqual expectedResult * 4 } + + _ <- IO { lastWrittenValue0 shouldBe n.some } + _ <- IO { lastWrittenValue1 shouldBe n.some } + _ <- IO { lastWrittenValue2 shouldBe n.some } + _ <- IO { lastWrittenValue3 shouldBe n.some } + + _ <- IO { lastValueRemoved0 shouldBe n.some } + _ <- IO { lastValueRemoved1 shouldBe n.some } + _ <- IO { lastValueRemoved2 shouldBe n.some } + _ <- IO { lastValueRemoved3 shouldBe n.some } + + _ <- metrics.expect( + metrics.expectedModify( + entryExisted = false, + CacheMetrics.Directive.Put + ) -> 4, + metrics.expectedModify( + entryExisted = true, + CacheMetrics.Directive.Put + ) -> (n - 1) * 4, + metrics.expectedModify( + entryExisted = true, + CacheMetrics.Directive.Remove + ) -> 4, + metrics.expectedGet(true) -> 4, + metrics.expectedLife -> n * 4 + ) + } yield () } } } object CacheSpec { - class CacheMetricsProbe(interactions: Ref[IO, Map[String, Int]]) extends CacheMetrics[IO] with Matchers with Eventually { - private def inc(op: String) = interactions.update(i => i.updated(op, i.getOrElse(op, 0) + 1)) + class CacheMetricsProbe(interactions: Ref[IO, Map[String, Int]]) + extends CacheMetrics[IO] + with Matchers + with Eventually { + private def inc(op: String) = + interactions.update(i => i.updated(op, i.getOrElse(op, 0) + 1)) // results of metrics.life calls might take some time - def expect(calls: (String, Int)*): IO[Assertion] = eventually(timeout(200.millis), interval(50.millis)) { - interactions - .get - .flatMap(is => IO { is shouldBe calls.toMap }) - } + def expect(calls: (String, Int)*): IO[Assertion] = + eventually(timeout(200.millis), interval(50.millis)) { + interactions.get + .flatMap(is => IO { is shouldBe calls.toMap }) + } def expectedGet(hit: Boolean): String = s"get(hit=$hit)" - def expectedLoad(success: Boolean): String = s"load(time=..., success=$success)" + def expectedLoad(success: Boolean): String = + s"load(time=..., success=$success)" val expectedLife: String = "life(time=...)" val expectedPut: String = "put" - def expectedModify(entryExisted: Boolean, directive: CacheMetrics.Directive): String = + def expectedModify( + entryExisted: Boolean, + directive: CacheMetrics.Directive + ): String = s"modify(existed=$entryExisted, directive=$directive" def expectedSize(size: Int): String = s"size(size=$size)" val expectedSize: String = "size(latency=...)" @@ -1538,10 +1718,15 @@ object CacheSpec { val expectedFoldMap: String = "foldMap(latency=...)" def get(hit: Boolean): IO[Unit] = inc(expectedGet(hit)) - def load(time: FiniteDuration, success: Boolean): IO[Unit] = inc(expectedLoad(success)) + def load(time: FiniteDuration, success: Boolean): IO[Unit] = inc( + expectedLoad(success) + ) def life(time: FiniteDuration): IO[Unit] = inc(expectedLife) def put: IO[Unit] = inc(expectedPut) - def modify(entryExisted: Boolean, directive: CacheMetrics.Directive): IO[Unit] = + def modify( + entryExisted: Boolean, + directive: CacheMetrics.Directive + ): IO[Unit] = inc(expectedModify(entryExisted, directive)) def size(size: Int): IO[Unit] = inc(expectedSize(size)) def size(latency: FiniteDuration): IO[Unit] = inc(expectedSize) @@ -1551,17 +1736,19 @@ object CacheSpec { def foldMap(latency: FiniteDuration): IO[Unit] = inc(expectedFoldMap) } object CacheMetricsProbe { - def of: IO[CacheMetricsProbe] = Ref.of[IO, Map[String, Int]](Map.empty).map(new CacheMetricsProbe(_)) + def of: IO[CacheMetricsProbe] = + Ref.of[IO, Map[String, Int]](Map.empty).map(new CacheMetricsProbe(_)) } case object TestError extends RuntimeException with NoStackTrace - implicit class CacheSpecCacheOps[F[_], K, V](val self: Cache[F, K, V]) extends AnyVal { + implicit class CacheSpecCacheOps[F[_], K, V](val self: Cache[F, K, V]) + extends AnyVal { def valuesFlatten(implicit F: Monad[F]): F[Map[K, V]] = { for { values <- self.values - zero = Map.empty[K, V].pure[F] + zero = Map.empty[K, V].pure[F] values <- values.foldLeft(zero) { case (map, (key, value)) => for { map <- map @@ -1573,11 +1760,12 @@ object CacheSpec { } yield values } - - def getOrUpdateEnsure(key: K)(value: => F[V])(implicit F: Concurrent[F]): F[Fiber[F, Throwable, Either[Throwable, V]]] = { + def getOrUpdateEnsure(key: K)(value: => F[V])(implicit + F: Concurrent[F] + ): F[Fiber[F, Throwable, Either[Throwable, V]]] = { for { deferred <- Deferred[F, Unit] - fiber <- self + fiber <- self .getOrUpdate(key) { for { _ <- deferred.complete(()) @@ -1586,14 +1774,16 @@ object CacheSpec { } .attempt .start - _ <- deferred.get + _ <- deferred.get } yield fiber } - def getOrUpdateOptEnsure(key: K)(value: => F[Option[V]])(implicit F: Concurrent[F]): F[Fiber[F, Throwable, Either[Throwable, Option[V]]]] = { + def getOrUpdateOptEnsure(key: K)(value: => F[Option[V]])(implicit + F: Concurrent[F] + ): F[Fiber[F, Throwable, Either[Throwable, Option[V]]]] = { for { deferred <- Deferred[F, Unit] - fiber <- self + fiber <- self .getOrUpdateOpt(key) { deferred .complete(()) @@ -1601,18 +1791,16 @@ object CacheSpec { } .attempt .start - _ <- deferred.get + _ <- deferred.get } yield fiber } - def getOrUpdate1Ensure( - key: K)( - value: => F[(V, Option[F[Unit]])])(implicit - F: Concurrent[F] + def getOrUpdate1Ensure(key: K)(value: => F[(V, Option[F[Unit]])])(implicit + F: Concurrent[F] ): F[Fiber[F, Throwable, Either[Throwable, V]]] = { for { deferred <- Deferred[F, Unit] - fiber <- self + fiber <- self .getOrUpdate1(key) { deferred .complete(()) @@ -1626,23 +1814,25 @@ object CacheSpec { } .attempt .start - _ <- deferred.get + _ <- deferred.get } yield fiber } def getOrUpdateOpt1Ensure( - key: K)( - value: => F[Option[(V, Option[F[Unit]])]])(implicit - F: Concurrent[F] + key: K + )(value: => F[Option[(V, Option[F[Unit]])]])(implicit + F: Concurrent[F] ): F[Fiber[F, Throwable, Option[V]]] = { for { deferred <- Deferred[F, Unit] - fiber <- self + fiber <- self .getOrUpdateOpt1[V](key) { deferred .complete(()) .productR { value } - .map { _.map { case (value, release) => (value, value, release) } } + .map { + _.map { case (value, release) => (value, value, release) } + } } .flatMap { case Some(Right(Right(a))) => a.some.pure[F] @@ -1651,7 +1841,7 @@ object CacheSpec { case None => none[V].pure[F] } .start - _ <- deferred.get + _ <- deferred.get } yield fiber } } diff --git a/src/test/scala/com/evolution/scache/ExpiringCacheSpec.scala b/src/test/scala/com/evolution/scache/ExpiringCacheSpec.scala index 5c2173d..fb0a2b9 100644 --- a/src/test/scala/com/evolution/scache/ExpiringCacheSpec.scala +++ b/src/test/scala/com/evolution/scache/ExpiringCacheSpec.scala @@ -43,93 +43,97 @@ class ExpiringCacheSpec extends AsyncFunSuite with Matchers { `refresh removes entry`[IO].run() } - private def expireRecords[F[_] : Async] = { + private def expireRecords[F[_]: Async] = { - ExpiringCache.of[F, Int, Int](ExpiringCache.Config(expireAfterRead = 100.millis)).use { cache => - for { - release <- Deferred[F, Unit] - value <- cache.put(0, 0, release.complete(()).void) - value <- value - _ <- Sync[F].delay { value shouldEqual none } - value <- cache.get(0) - _ <- Sync[F].delay { value shouldEqual 0.some } - _ <- release.get - value <- cache.get(0) - _ <- Sync[F].delay { value shouldEqual none } - } yield {} - } + ExpiringCache + .of[F, Int, Int](ExpiringCache.Config(expireAfterRead = 100.millis)) + .use { cache => + for { + release <- Deferred[F, Unit] + value <- cache.put(0, 0, release.complete(()).void) + value <- value + _ <- Sync[F].delay { value shouldEqual none } + value <- cache.get(0) + _ <- Sync[F].delay { value shouldEqual 0.some } + _ <- release.get + value <- cache.get(0) + _ <- Sync[F].delay { value shouldEqual none } + } yield {} + } } - private def `expire created entries`[F[_] : Async] = { - val config = ExpiringCache.Config[F, Int, Int]( + private def `expire created entries`[F[_]: Async] = { + val config = ExpiringCache.Config[F, Int, Int]( expireAfterRead = 1.minute, - expireAfterWrite = 150.millis.some) + expireAfterWrite = 150.millis.some + ) ExpiringCache.of[F, Int, Int](config).use { cache => for { release <- Deferred[F, Unit] - _ <- cache.put(0, 0, release.complete(()).void) - _ <- Temporal[F].sleep(50.millis) - value <- cache.get(0) - _ <- Sync[F].delay { value shouldEqual 0.some } - _ <- release.get - value <- cache.get(0) - _ <- Sync[F].delay { value shouldEqual none } + _ <- cache.put(0, 0, release.complete(()).void) + _ <- Temporal[F].sleep(50.millis) + value <- cache.get(0) + _ <- Sync[F].delay { value shouldEqual 0.some } + _ <- release.get + value <- cache.get(0) + _ <- Sync[F].delay { value shouldEqual none } } yield {} } } - private def notExpireUsedRecords[F[_] : Async] = { - ExpiringCache.of[F, Int, Int](ExpiringCache.Config(50.millis)).use { cache => - val touch = for { - _ <- Temporal[F].sleep(10.millis) - _ <- cache.get(0) - } yield {} - for { - release <- Ref[F].of(false) - value <- cache.put(0, 0, release.set(true)) - value <- value - _ <- Sync[F].delay { value shouldEqual none } - value <- cache.put(1, 1) - value <- value - _ <- Sync[F].delay { value shouldEqual none } - _ <- List.fill(6)(touch).foldMapM(identity) - value <- cache.get(0) - _ <- Sync[F].delay { value shouldEqual 0.some } - value <- cache.get(1) - _ <- Sync[F].delay { value shouldEqual none } - release <- release.get - _ <- Sync[F].delay { release shouldEqual false} - } yield {} + private def notExpireUsedRecords[F[_]: Async] = { + ExpiringCache.of[F, Int, Int](ExpiringCache.Config(50.millis)).use { + cache => + val touch = for { + _ <- Temporal[F].sleep(10.millis) + _ <- cache.get(0) + } yield {} + for { + release <- Ref[F].of(false) + value <- cache.put(0, 0, release.set(true)) + value <- value + _ <- Sync[F].delay { value shouldEqual none } + value <- cache.put(1, 1) + value <- value + _ <- Sync[F].delay { value shouldEqual none } + _ <- List.fill(6)(touch).foldMapM(identity) + value <- cache.get(0) + _ <- Sync[F].delay { value shouldEqual 0.some } + value <- cache.get(1) + _ <- Sync[F].delay { value shouldEqual none } + release <- release.get + _ <- Sync[F].delay { release shouldEqual false } + } yield {} } } - - private def notExceedMaxSize[F[_] : Async] = { + private def notExceedMaxSize[F[_]: Async] = { val config = ExpiringCache.Config[F, Int, Int]( expireAfterRead = 100.millis, expireAfterWrite = 100.millis.some, - maxSize = 10.some) + maxSize = 10.some + ) ExpiringCache.of(config).use { cache => for { release <- Deferred[F, Unit] - _ <- cache.put(0, 0, release.complete(()).void) - _ <- (1 until 10).toList.foldMapM { n => cache.put(n, n).void } - value <- cache.get(0) - _ <- Sync[F].delay { value shouldEqual 0.some } - _ <- cache.put(10, 10) - _ <- release.get + _ <- cache.put(0, 0, release.complete(()).void) + _ <- (1 until 10).toList.foldMapM { n => cache.put(n, n).void } + value <- cache.get(0) + _ <- Sync[F].delay { value shouldEqual 0.some } + _ <- cache.put(10, 10) + _ <- release.get } yield {} } } - private def refreshPeriodically[F[_] : Async] = { + private def refreshPeriodically[F[_]: Async] = { val refresh = ExpiringCache.Refresh[Int](100.millis) { _.some.pure[F] } val config = ExpiringCache.Config( expireAfterRead = 1.minute, expireAfterWrite = 1.minute.some, - refresh = refresh.some) + refresh = refresh.some + ) ExpiringCache.of[F, Int, Int](config).use { cache => - def retryUntilRefreshed(key: Int, original: Int) = { Retry(10.millis, 100) { for { @@ -143,24 +147,22 @@ class ExpiringCacheSpec extends AsyncFunSuite with Matchers { for { value <- cache.put(0, 1) value <- value - _ <- Sync[F].delay { value shouldEqual none } + _ <- Sync[F].delay { value shouldEqual none } value <- cache.get(0) - _ <- Sync[F].delay { value shouldEqual 1.some } + _ <- Sync[F].delay { value shouldEqual 1.some } value <- retryUntilRefreshed(0, 1) - _ <- Sync[F].delay { value shouldEqual 0.some } + _ <- Sync[F].delay { value shouldEqual 0.some } } yield {} } } - private def refreshDoesNotTouch[F[_] : Async] = { + private def refreshDoesNotTouch[F[_]: Async] = { val refresh = ExpiringCache.Refresh[Int](100.millis) { _.some.pure[F] } - val config = ExpiringCache.Config( - expireAfterRead = 100.millis, - refresh = refresh.some) + val config = + ExpiringCache.Config(expireAfterRead = 100.millis, refresh = refresh.some) ExpiringCache.of[F, Int, Int](config).use { cache => - def retryUntilRefreshed(key: Int, original: Int) = { Retry(10.millis, 100) { for { @@ -173,25 +175,26 @@ class ExpiringCacheSpec extends AsyncFunSuite with Matchers { for { released <- Ref[F].of(false) - release <- Deferred[F, Unit] - value <- cache.put(0, 1, released.set(true) *> release.complete(()).void) - value <- value - _ <- Sync[F].delay { value shouldEqual none } - value <- cache.get(0) - _ <- Sync[F].delay { value shouldEqual 1.some } - value <- retryUntilRefreshed(0, 1) + release <- Deferred[F, Unit] + value <- cache + .put(0, 1, released.set(true) *> release.complete(()).void) + value <- value + _ <- Sync[F].delay { value shouldEqual none } + value <- cache.get(0) + _ <- Sync[F].delay { value shouldEqual 1.some } + value <- retryUntilRefreshed(0, 1) released <- released.get - _ <- Sync[F].delay { released shouldEqual false} - _ <- Sync[F].delay { value shouldEqual 0.some } - _ <- release.get + _ <- Sync[F].delay { released shouldEqual false } + _ <- Sync[F].delay { value shouldEqual 0.some } + _ <- release.get } yield {} } } - private def refreshFails[F[_] : Async] = { + private def refreshFails[F[_]: Async] = { - def valueOf(ref: Ref[F, Int]) = { - (_: Int) => { + def valueOf(ref: Ref[F, Int]) = { (_: Int) => + { for { n <- ref.modify { n => (n + 1, n) } v <- if (n == 0) TestError.raiseError[F, Int] else 1.pure[F] @@ -202,15 +205,15 @@ class ExpiringCacheSpec extends AsyncFunSuite with Matchers { } for { - ref <- Ref[F].of(0) - value = valueOf(ref) - refresh = ExpiringCache.Refresh(50.millis, value) - config = ExpiringCache.Config( + ref <- Ref[F].of(0) + value = valueOf(ref) + refresh = ExpiringCache.Refresh(50.millis, value) + config = ExpiringCache.Config( expireAfterRead = 1.minute, expireAfterWrite = 1.minute.some, - refresh = refresh.some) - result <- ExpiringCache.of(config).use { cache => - + refresh = refresh.some + ) + result <- ExpiringCache.of(config).use { cache => def retryUntilRefreshed(key: Int, original: Int) = { Retry(10.millis, 100) { for { @@ -224,27 +227,27 @@ class ExpiringCacheSpec extends AsyncFunSuite with Matchers { for { value <- cache.put(0, 0) value <- value - _ <- Sync[F].delay { value shouldEqual none } + _ <- Sync[F].delay { value shouldEqual none } value <- cache.get(0) - _ <- Sync[F].delay { value shouldEqual 0.some } + _ <- Sync[F].delay { value shouldEqual 0.some } value <- retryUntilRefreshed(0, 0) - _ <- Sync[F].delay { value shouldEqual 1.some } + _ <- Sync[F].delay { value shouldEqual 1.some } value <- ref.get - _ <- Sync[F].delay { value should be >= 1 } + _ <- Sync[F].delay { value should be >= 1 } } yield {} } } yield result } - def `refresh removes entry`[F[_] : Async] = { - val refresh = ExpiringCache.Refresh[Int](100.millis) { _ => none[Int].pure[F] } + def `refresh removes entry`[F[_]: Async] = { + val refresh = ExpiringCache.Refresh[Int](100.millis) { _ => + none[Int].pure[F] + } - val config = ExpiringCache.Config( - expireAfterRead = 100.millis, - refresh = refresh.some) + val config = + ExpiringCache.Config(expireAfterRead = 100.millis, refresh = refresh.some) ExpiringCache.of[F, Int, Int](config).use { cache => - def retryUntilNone(key: Int) = { 0.tailRecM[F, Option[Int]] { round => for { @@ -268,37 +271,36 @@ class ExpiringCacheSpec extends AsyncFunSuite with Matchers { for { released <- Deferred[F, Boolean] - release <- Deferred[F, Unit] - value <- cache.put(0, 1, released.complete(true) *> release.complete(()).void) - value <- value - _ <- Sync[F].delay { value shouldEqual none } - value <- cache.get(0) - _ <- Sync[F].delay { value shouldEqual 1.some } - value <- retryUntilNone(0) + release <- Deferred[F, Unit] + value <- cache + .put(0, 1, released.complete(true) *> release.complete(()).void) + value <- value + _ <- Sync[F].delay { value shouldEqual none } + value <- cache.get(0) + _ <- Sync[F].delay { value shouldEqual 1.some } + value <- retryUntilNone(0) released <- released.get - _ <- Sync[F].delay { released shouldEqual true} - _ <- Sync[F].delay { value shouldEqual none } - _ <- release.get + _ <- Sync[F].delay { released shouldEqual true } + _ <- Sync[F].delay { value shouldEqual none } + _ <- release.get } yield {} } } - object Retry { - def apply[F[_] : Temporal, A]( - delay: FiniteDuration, - times: Int)( - fa: F[Option[A]] + def apply[F[_]: Temporal, A](delay: FiniteDuration, times: Int)( + fa: F[Option[A]] ): F[Option[A]] = { def retry(round: Int) = { if (round >= times) none[A].asRight[Int].pure[F] - else for { - _ <- Temporal[F].sleep(delay) - } yield { - (round + 1).asLeft[Option[A]] - } + else + for { + _ <- Temporal[F].sleep(delay) + } yield { + (round + 1).asLeft[Option[A]] + } } 0.tailRecM[F, Option[A]] { round => @@ -311,4 +313,4 @@ class ExpiringCacheSpec extends AsyncFunSuite with Matchers { } case object TestError extends RuntimeException with NoStackTrace -} \ No newline at end of file +} diff --git a/src/test/scala/com/evolution/scache/IOSuite.scala b/src/test/scala/com/evolution/scache/IOSuite.scala index ea5d940..cdcd25c 100644 --- a/src/test/scala/com/evolution/scache/IOSuite.scala +++ b/src/test/scala/com/evolution/scache/IOSuite.scala @@ -15,21 +15,32 @@ object IOSuite { val Timeout: FiniteDuration = 5.seconds implicit val executor: ExecutionContextExecutor = ExecutionContext.global - implicit val measureDuration: MeasureDuration[IO] = MeasureDuration.fromClock(Clock[IO]) + implicit val measureDuration: MeasureDuration[IO] = + MeasureDuration.fromClock(Clock[IO]) // Allows to use Eventually methods with IO implicit def ioRetrying[T]: Retrying[IO[T]] = new Retrying[IO[T]] { - override def retry(timeout: Span, interval: Span, pos: Position)(f: => IO[T]): IO[T] = + override def retry(timeout: Span, interval: Span, pos: Position)( + f: => IO[T] + ): IO[T] = IO.fromFuture( - IO(Retrying.retryingNatureOfFutureT[T](executor).retry(timeout, interval, pos)(f.unsafeToFuture())), + IO( + Retrying + .retryingNatureOfFutureT[T](executor) + .retry(timeout, interval, pos)(f.unsafeToFuture()) + ) ) } - def runIO[A](io: IO[A], timeout: FiniteDuration = Timeout): Future[Succeeded.type] = { + def runIO[A]( + io: IO[A], + timeout: FiniteDuration = Timeout + ): Future[Succeeded.type] = { io.timeout(timeout).as(Succeeded).unsafeToFuture() } implicit class IOOps[A](val self: IO[A]) extends AnyVal { - def run(timeout: FiniteDuration = Timeout): Future[Succeeded.type] = runIO(self, timeout) + def run(timeout: FiniteDuration = Timeout): Future[Succeeded.type] = + runIO(self, timeout) } -} \ No newline at end of file +} diff --git a/src/test/scala/com/evolution/scache/PartitionsSpec.scala b/src/test/scala/com/evolution/scache/PartitionsSpec.scala index 97f570d..0a5241f 100644 --- a/src/test/scala/com/evolution/scache/PartitionsSpec.scala +++ b/src/test/scala/com/evolution/scache/PartitionsSpec.scala @@ -26,7 +26,6 @@ class PartitionsSpec extends AnyWordSpec with Matchers { } } - "const" should { val partitions = Partitions.const[Int, String]("0") diff --git a/src/test/scala/com/evolution/scache/SerialMapSpec.scala b/src/test/scala/com/evolution/scache/SerialMapSpec.scala index 026b3d8..a198b75 100644 --- a/src/test/scala/com/evolution/scache/SerialMapSpec.scala +++ b/src/test/scala/com/evolution/scache/SerialMapSpec.scala @@ -69,57 +69,59 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { `modify in parallel for different keys`[IO].run() } - private def get[F[_] : Async] = { + private def get[F[_]: Async] = { val key = "key" for { serialMap <- SerialMap.of[F, String, Int] - value0 <- serialMap.get(key) - _ <- serialMap.put(key, 0) - value1 <- serialMap.get(key) + value0 <- serialMap.get(key) + _ <- serialMap.put(key, 0) + value1 <- serialMap.get(key) } yield { value0 shouldEqual none[Int] value1 shouldEqual 0.some } } - private def getOrElse[F[_] : Async] = { + private def getOrElse[F[_]: Async] = { val key = "key" for { serialMap <- SerialMap.of[F, String, Int] - value0 <- serialMap.getOrElse(key, 1.pure[F]) - _ <- serialMap.put(key, 2) - value1 <- serialMap.getOrElse(key, 1.pure[F]) + value0 <- serialMap.getOrElse(key, 1.pure[F]) + _ <- serialMap.put(key, 2) + value1 <- serialMap.getOrElse(key, 1.pure[F]) } yield { value0 shouldEqual 1 value1 shouldEqual 2 } } - private def getOrUpdate[F[_] : Async] = { + private def getOrUpdate[F[_]: Async] = { val key = "key" for { serialMap <- SerialMap.of[F, String, Int] - started <- Deferred[F, Unit] - deferred <- Deferred[F, Int] - value0 <- serialMap.getOrUpdate(key, started.complete(()) *> deferred.get).startEnsure - _ <- started.get - value1 <- serialMap.getOrUpdate(key, 1.pure[F]).startEnsure - _ <- deferred.complete(0) - value0 <- value0.join - value1 <- value1.join + started <- Deferred[F, Unit] + deferred <- Deferred[F, Int] + value0 <- serialMap + .getOrUpdate(key, started.complete(()) *> deferred.get) + .startEnsure + _ <- started.get + value1 <- serialMap.getOrUpdate(key, 1.pure[F]).startEnsure + _ <- deferred.complete(0) + value0 <- value0.join + value1 <- value1.join } yield { value0 shouldEqual Outcome.succeeded(IO.pure(0)) value1 shouldEqual Outcome.succeeded(IO.pure(0)) } } - private def put[F[_] : Async] = { + private def put[F[_]: Async] = { val key = "key" for { serialMap <- SerialMap.of[F, String, Int] - value0 <- serialMap.put(key, 0) - value1 <- serialMap.put(key, 1) - value2 <- serialMap.put(key, 2) + value0 <- serialMap.put(key, 0) + value1 <- serialMap.put(key, 1) + value2 <- serialMap.put(key, 2) } yield { value0 shouldEqual none[Int] value1 shouldEqual 0.some @@ -127,18 +129,18 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { } } - private def modify[F[_] : Async] = { + private def modify[F[_]: Async] = { val key = "key" for { serialMap <- SerialMap.of[F, String, Int] - value0 <- serialMap.modify(key) { value => + value0 <- serialMap.modify(key) { value => val value1 = value.fold(0) { _ + 1 } (value1.some, value).pure[F] } - value1 <- serialMap.modify(key) { value => + value1 <- serialMap.modify(key) { value => (none[Int], value).pure[F] } - value2 <- serialMap.modify(key) { value => + value2 <- serialMap.modify(key) { value => (none[Int], value).pure[F] } } yield { @@ -148,17 +150,16 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { } } - - private def update[F[_] : Async] = { + private def update[F[_]: Async] = { val key = "key" for { serialMap <- SerialMap.of[F, String, Int] - _ <- serialMap.update(key) { _.fold(0) { _ + 1 }.some.pure[F] } - value0 <- serialMap.get(key) - _ <- serialMap.update(key) { _ => none[Int].pure[F] } - value1 <- serialMap.get(key) - _ <- serialMap.update(key) { _ => none[Int].pure[F] } - value2 <- serialMap.get(key) + _ <- serialMap.update(key) { _.fold(0) { _ + 1 }.some.pure[F] } + value0 <- serialMap.get(key) + _ <- serialMap.update(key) { _ => none[Int].pure[F] } + value1 <- serialMap.get(key) + _ <- serialMap.update(key) { _ => none[Int].pure[F] } + value2 <- serialMap.get(key) } yield { value0 shouldEqual 0.some value1 shouldEqual none[Int] @@ -166,21 +167,20 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { } } - - private def size[F[_] : Async] = { + private def size[F[_]: Async] = { for { serialMap <- SerialMap.of[F, Int, Int] - size0 <- serialMap.size - _ <- serialMap.put(0, 0) - size1 <- serialMap.size - _ <- serialMap.put(0, 1) - size2 <- serialMap.size - _ <- serialMap.put(1, 1) - size3 <- serialMap.size - _ <- serialMap.remove(0) - size4 <- serialMap.size - _ <- serialMap.clear - size5 <- serialMap.size + size0 <- serialMap.size + _ <- serialMap.put(0, 0) + size1 <- serialMap.size + _ <- serialMap.put(0, 1) + size2 <- serialMap.size + _ <- serialMap.put(1, 1) + size3 <- serialMap.size + _ <- serialMap.remove(0) + size4 <- serialMap.size + _ <- serialMap.clear + size5 <- serialMap.size } yield { size0 shouldEqual 0 size1 shouldEqual 1 @@ -191,19 +191,18 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { } } - - private def keys[F[_] : Async] = { + private def keys[F[_]: Async] = { for { serialMap <- SerialMap.of[F, Int, Int] - _ <- serialMap.put(0, 0) - keys = serialMap.keys - keys0 <- keys - _ <- serialMap.put(1, 1) - keys1 <- keys - _ <- serialMap.put(2, 2) - keys2 <- keys - _ <- serialMap.clear - keys3 <- keys + _ <- serialMap.put(0, 0) + keys = serialMap.keys + keys0 <- keys + _ <- serialMap.put(1, 1) + keys1 <- keys + _ <- serialMap.put(2, 2) + keys2 <- keys + _ <- serialMap.clear + keys3 <- keys } yield { keys0 shouldEqual Set(0) keys1 shouldEqual Set(0, 1) @@ -212,18 +211,17 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { } } - - private def values[F[_] : Async] = { + private def values[F[_]: Async] = { for { serialMap <- SerialMap.of[F, Int, Int] - _ <- serialMap.put(0, 0) - values0 <- serialMap.values - _ <- serialMap.put(1, 1) - values1 <- serialMap.values - _ <- serialMap.put(2, 2) - values2 <- serialMap.values - _ <- serialMap.clear - values3 <- serialMap.values + _ <- serialMap.put(0, 0) + values0 <- serialMap.values + _ <- serialMap.put(1, 1) + values1 <- serialMap.values + _ <- serialMap.put(2, 2) + values2 <- serialMap.values + _ <- serialMap.clear + values3 <- serialMap.values } yield { values0 shouldEqual Map((0, 0)) values1 shouldEqual Map((0, 0), (1, 1)) @@ -232,15 +230,17 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { } } - - private def remove[F[_] : Concurrent] = { + private def remove[F[_]: Concurrent] = { val key = "key" - val cache = LoadingCache.of(LoadingCache.EntryRefs.empty[F, String, SerialRef[F, SerialMap.State[Int]]]) + val cache = LoadingCache.of( + LoadingCache.EntryRefs + .empty[F, String, SerialRef[F, SerialMap.State[Int]]] + ) cache.use { cache => val serialMap = SerialMap(cache) for { value0 <- cache.get(key) - _ <- serialMap.update(key) { _ => 0.some.pure[F] } + _ <- serialMap.update(key) { _ => 0.some.pure[F] } value1 <- cache.get(key) value2 <- serialMap.remove(key) value3 <- cache.get(key) @@ -253,32 +253,35 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { } } - - private def clear[F[_] : Async] = { + private def clear[F[_]: Async] = { for { serialMap <- SerialMap.of[F, Int, Int] - _ <- serialMap.put(0, 0) - _ <- serialMap.put(1, 1) - _ <- serialMap.clear - value0 <- serialMap.get(0) - value1 <- serialMap.get(1) + _ <- serialMap.put(0, 0) + _ <- serialMap.put(1, 1) + _ <- serialMap.clear + value0 <- serialMap.get(0) + value1 <- serialMap.get(1) } yield { value0 shouldEqual none[Int] value1 shouldEqual none[Int] } } - - private def `not leak on failures`[F[_] : Concurrent] = { + private def `not leak on failures`[F[_]: Concurrent] = { val key = "key" - val cache = LoadingCache.of(LoadingCache.EntryRefs.empty[F, String, SerialRef[F, SerialMap.State[Int]]]) + val cache = LoadingCache.of( + LoadingCache.EntryRefs + .empty[F, String, SerialRef[F, SerialMap.State[Int]]] + ) cache.use { cache => val serialMap = SerialMap(cache) - val modifyError = serialMap.modify(key) { _ => TestError.raiseError[F, (Option[Int], Unit)] }.attempt + val modifyError = serialMap + .modify(key) { _ => TestError.raiseError[F, (Option[Int], Unit)] } + .attempt for { value0 <- modifyError value1 <- cache.get(key) - _ <- serialMap.put(key, 0) + _ <- serialMap.put(key, 0) value2 <- modifyError value3 <- serialMap.get(key) } yield { @@ -290,14 +293,13 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { } } - - private def `modify serially for the same key`[F[_] : Async] = { + private def `modify serially for the same key`[F[_]: Async] = { val key = "key" for { serialMap <- SerialMap.of[F, String, Int] - blocked <- Deferred[F, Unit] - acquired <- Deferred[F, Unit] - value0 = serialMap.modify(key) { value => + blocked <- Deferred[F, Unit] + acquired <- Deferred[F, Unit] + value0 = serialMap.modify(key) { value => for { _ <- acquired.complete(()) _ <- blocked.get @@ -305,26 +307,25 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { inc(value) } } - value0 <- value0.startEnsure - _ <- acquired.get - value1 = serialMap.modify(key) { value => inc(value).pure[F] } - value1 <- value1.startEnsure - _ <- blocked.complete(()) - value0 <- value0.join - value1 <- value1.join + value0 <- value0.startEnsure + _ <- acquired.get + value1 = serialMap.modify(key) { value => inc(value).pure[F] } + value1 <- value1.startEnsure + _ <- blocked.complete(()) + value0 <- value0.join + value1 <- value1.join } yield { value0 shouldEqual Outcome.succeeded(IO.pure(0)) value1 shouldEqual Outcome.succeeded(IO.pure(1)) } } - - private def `modify in parallel for different keys`[F[_] : Async] = { + private def `modify in parallel for different keys`[F[_]: Async] = { for { serialMap <- SerialMap.of[F, String, Int] - blocked <- Deferred[F, Unit] - acquired <- Deferred[F, Unit] - value0 = serialMap.modify("key1") { value => + blocked <- Deferred[F, Unit] + acquired <- Deferred[F, Unit] + value0 = serialMap.modify("key1") { value => for { _ <- acquired.complete(()) _ <- blocked.get @@ -332,11 +333,11 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { inc(value) } } - value0 <- value0.startEnsure - _ <- acquired.get - value1 <- serialMap.modify("key2") { v => inc(v).pure[F] } - _ <- blocked.complete(()) - value0 <- value0.join + value0 <- value0.startEnsure + _ <- acquired.get + value1 <- serialMap.modify("key2") { v => inc(v).pure[F] } + _ <- blocked.complete(()) + value0 <- value0.join } yield { value0 shouldEqual Outcome.succeeded(IO.pure(0)) value1 shouldEqual 0