From ac2133148519f2cc516406ed664962bb75577a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Fri, 7 Feb 2020 02:13:58 +0100 Subject: [PATCH 01/21] Add cats-effect to the core module, remove modes --- build.sbt | 12 +- .../main/scala/scalacache/CatsEffect.scala | 45 ------ .../main/scala/scalacache/AbstractCache.scala | 121 +++++++--------- .../src/main/scala/scalacache/Async.scala | 133 ------------------ .../src/main/scala/scalacache/Cache.scala | 11 +- .../src/main/scala/scalacache/CacheAlg.scala | 32 ++--- .../src/main/scala/scalacache/Mode.scala | 68 --------- .../scala/scalacache/memoization/Macros.scala | 20 +-- .../scalacache/memoization/package.scala | 29 +--- .../src/main/scala/scalacache/package.scala | 55 ++------ modules/docs/src/main/mdoc/docs/modes.md | 98 ------------- modules/docs/src/main/mdoc/docs/sync-api.md | 40 ------ 12 files changed, 91 insertions(+), 573 deletions(-) delete mode 100644 modules/cats-effect/src/main/scala/scalacache/CatsEffect.scala delete mode 100644 modules/core/shared/src/main/scala/scalacache/Async.scala delete mode 100644 modules/core/shared/src/main/scala/scalacache/Mode.scala delete mode 100644 modules/docs/src/main/mdoc/docs/modes.md delete mode 100644 modules/docs/src/main/mdoc/docs/sync-api.md diff --git a/build.sbt b/build.sbt index c6029260..5b80ba5c 100644 --- a/build.sbt +++ b/build.sbt @@ -30,6 +30,7 @@ lazy val root: Project = Project(id = "scalacache", base = file(".")) redis, caffeine, catsEffect, + ohc, scalaz72, circe, tests @@ -42,6 +43,7 @@ lazy val core = moduleName := "scalacache-core", libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value, + "org.typelevel" %% "cats-effect" % "2.1.0", "org.scalatest" %%% "scalatest" % "3.0.8" % Test, "org.scalacheck" %%% "scalacheck" % "1.14.3" % Test ), @@ -105,6 +107,13 @@ lazy val catsEffect = jvmOnlyModule("cats-effect") coverageFailOnMinimum := true ) +lazy val ohc = jvmOnlyModule("ohc") + .settings( + libraryDependencies ++= Seq( + "org.caffinitas.ohc" % "ohc-core" % "0.7.0" + ) + ) + lazy val scalaz72 = jvmOnlyModule("scalaz72") .settings( libraryDependencies ++= Seq( @@ -136,7 +145,7 @@ lazy val circe = jvmOnlyModule("circe") lazy val tests = jvmOnlyModule("tests") .settings(publishArtifact := false) - .dependsOn(caffeine, memcached, redis, catsEffect, scalaz72, circe) + .dependsOn(cache2k, caffeine, memcached, redis, ohc, scalaz72, circe, catsEffect) lazy val docs = jvmOnlyModule("docs") .enablePlugins(MicrositesPlugin) @@ -161,6 +170,7 @@ lazy val docs = jvmOnlyModule("docs") redis, caffeine, catsEffect, + ohc, scalaz72, circe ) diff --git a/modules/cats-effect/src/main/scala/scalacache/CatsEffect.scala b/modules/cats-effect/src/main/scala/scalacache/CatsEffect.scala deleted file mode 100644 index d73907e5..00000000 --- a/modules/cats-effect/src/main/scala/scalacache/CatsEffect.scala +++ /dev/null @@ -1,45 +0,0 @@ -package scalacache - -import cats.effect.{Async => CatsAsync, IO} - -import scala.language.higherKinds -import scala.util.control.NonFatal - -object CatsEffect { - - object modes { - - /** - * A mode that wraps computations in F[_], - * where there is an instance of cats-effect Async available for F. - * This includes the cats-effect `IO[_]` type. - */ - implicit def async[F[_]](implicit F: CatsAsync[F]): Mode[F] = new Mode[F] { - val M: Async[F] = asyncForCatsEffectAsync[F] - } - - } - - def asyncForCatsEffectAsync[F[_]](implicit af: CatsAsync[F]): Async[F] = new Async[F] { - - def pure[A](a: A): F[A] = af.pure(a) - - def flatMap[A, B](fa: F[A])(f: (A) => F[B]): F[B] = af.flatMap(fa)(f) - - def map[A, B](fa: F[A])(f: (A) => B): F[B] = af.map(fa)(f) - - def raiseError[A](t: Throwable): F[A] = af.raiseError(t) - - def handleNonFatal[A](fa: => F[A])(f: Throwable => A): F[A] = af.recover(fa) { - case NonFatal(e) => f(e) - } - - def delay[A](thunk: => A): F[A] = af.delay(thunk) - - def suspend[A](thunk: => F[A]): F[A] = af.suspend(thunk) - - def async[A](register: (Either[Throwable, A] => Unit) => Unit): F[A] = af.async(register) - - } - -} diff --git a/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala b/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala index 4046269b..eb5ebccb 100644 --- a/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala +++ b/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala @@ -3,6 +3,10 @@ package scalacache import scala.concurrent.duration.Duration import scala.language.higherKinds +import cats.Monad +import cats.implicits._ +import cats.MonadError +import cats.Defer /** * An abstract implementation of [[CacheAlg]] that takes care of @@ -13,34 +17,36 @@ import scala.language.higherKinds * * @tparam V The value of types stored in the cache. */ -trait AbstractCache[V] extends Cache[V] with LoggingSupport { +trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { + protected implicit def F: MonadError[F, Throwable] + protected implicit def Defer: Defer[F] // GET - protected def doGet[F[_]](key: String)(implicit mode: Mode[F]): F[Option[V]] + protected def doGet(key: String): F[Option[V]] - private def checkFlagsAndGet[F[_]](key: String)(implicit mode: Mode[F], flags: Flags): F[Option[V]] = { + private def checkFlagsAndGet(key: String)(implicit flags: Flags): F[Option[V]] = { if (flags.readsEnabled) { doGet(key) } else { if (logger.isDebugEnabled) { logger.debug(s"Skipping cache GET because cache reads are disabled. Key: $key") } - mode.M.pure(None) + F.pure(None) } } - final override def get[F[_]](keyParts: Any*)(implicit mode: Mode[F], flags: Flags): F[Option[V]] = { + final override def get(keyParts: Any*)(implicit flags: Flags): F[Option[V]] = { val key = toKey(keyParts: _*) checkFlagsAndGet(key) } // PUT - protected def doPut[F[_]](key: String, value: V, ttl: Option[Duration])(implicit mode: Mode[F]): F[Any] + protected def doPut(key: String, value: V, ttl: Option[Duration]): F[Any] - private def checkFlagsAndPut[F[_]](key: String, value: V, ttl: Option[Duration])( - implicit mode: Mode[F], + private def checkFlagsAndPut(key: String, value: V, ttl: Option[Duration])( + implicit flags: Flags ): F[Any] = { if (flags.writesEnabled) { @@ -49,13 +55,13 @@ trait AbstractCache[V] extends Cache[V] with LoggingSupport { if (logger.isDebugEnabled) { logger.debug(s"Skipping cache PUT because cache writes are disabled. Key: $key") } - mode.M.pure(()) + F.pure(()) } } - final override def put[F[_]]( + final override def put( keyParts: Any* - )(value: V, ttl: Option[Duration])(implicit mode: Mode[F], flags: Flags): F[Any] = { + )(value: V, ttl: Option[Duration])(implicit flags: Flags): F[Any] = { val key = toKey(keyParts: _*) val finiteTtl = ttl.filter(_.isFinite) // discard Duration.Inf, Duration.Undefined checkFlagsAndPut(key, value, finiteTtl) @@ -63,109 +69,78 @@ trait AbstractCache[V] extends Cache[V] with LoggingSupport { // REMOVE - protected def doRemove[F[_]](key: String)(implicit mode: Mode[F]): F[Any] + protected def doRemove(key: String): F[Any] - final override def remove[F[_]](keyParts: Any*)(implicit mode: Mode[F]): F[Any] = + final override def remove(keyParts: Any*): F[Any] = doRemove(toKey(keyParts: _*)) // REMOVE ALL - protected def doRemoveAll[F[_]]()(implicit mode: Mode[F]): F[Any] + protected def doRemoveAll: F[Any] - final override def removeAll[F[_]]()(implicit mode: Mode[F]): F[Any] = - doRemoveAll() + final override def removeAll: F[Any] = + doRemoveAll // CACHING - final override def caching[F[_]]( + final override def caching( keyParts: Any* - )(ttl: Option[Duration] = None)(f: => V)(implicit mode: Mode[F], flags: Flags): F[V] = { + )(ttl: Option[Duration] = None)(f: => V)(implicit flags: Flags): F[V] = { val key = toKey(keyParts: _*) _caching(key, ttl, f) } - override def cachingF[F[_]]( + override def cachingF( keyParts: Any* - )(ttl: Option[Duration] = None)(f: => F[V])(implicit mode: Mode[F], flags: Flags): F[V] = { + )(ttl: Option[Duration] = None)(f: F[V])(implicit flags: Flags): F[V] = { val key = toKey(keyParts: _*) _cachingF(key, ttl, f) } // MEMOIZE - override def cachingForMemoize[F[_]]( + override def cachingForMemoize( baseKey: String - )(ttl: Option[Duration] = None)(f: => V)(implicit mode: Mode[F], flags: Flags): F[V] = { + )(ttl: Option[Duration] = None)(f: => V)(implicit flags: Flags): F[V] = { val key = config.cacheKeyBuilder.stringToCacheKey(baseKey) _caching(key, ttl, f) } - override def cachingForMemoizeF[F[_]]( + override def cachingForMemoizeF( baseKey: String - )(ttl: Option[Duration])(f: => F[V])(implicit mode: Mode[F], flags: Flags): F[V] = { + )(ttl: Option[Duration])(f: F[V])(implicit flags: Flags): F[V] = { val key = config.cacheKeyBuilder.stringToCacheKey(baseKey) _cachingF(key, ttl, f) } - private def _caching[F[_]](key: String, ttl: Option[Duration], f: => V)( - implicit mode: Mode[F], + private def _caching(key: String, ttl: Option[Duration], f: => V)( + implicit flags: Flags - ): F[V] = { - import mode._ - - M.flatMap { - M.handleNonFatal(checkFlagsAndGet(key)) { e => - if (logger.isWarnEnabled) { - logger.warn(s"Failed to read from cache. Key = $key", e) - } - None - } - } { - case Some(valueFromCache) => - M.pure(valueFromCache) - case None => - val calculatedValue = f - M.map { - M.handleNonFatal { - checkFlagsAndPut(key, calculatedValue, ttl) - } { e => - if (logger.isWarnEnabled) { - logger.warn(s"Failed to write to cache. Key = $key", e) - } - } - }(_ => calculatedValue) - } - } + ): F[V] = _cachingF(key, ttl, Defer.defer(F.pure(f))) - private def _cachingF[F[_]](key: String, ttl: Option[Duration], f: => F[V])( - implicit mode: Mode[F], + private def _cachingF(key: String, ttl: Option[Duration], f: => F[V])( + implicit flags: Flags ): F[V] = { - import mode._ - - M.flatMap { - M.handleNonFatal(checkFlagsAndGet(key)) { e => + checkFlagsAndGet(key) + .handleError { e => if (logger.isWarnEnabled) { logger.warn(s"Failed to read from cache. Key = $key", e) } None } - } { - case Some(valueFromCache) => - M.pure(valueFromCache) - case None => - M.flatMap(f) { calculatedValue => - M.map { - M.handleNonFatal { - checkFlagsAndPut(key, calculatedValue, ttl) - } { e => - if (logger.isWarnEnabled) { - logger.warn(s"Failed to write to cache. Key = $key", e) + .flatMap { + case Some(valueFromCache) => F.pure(valueFromCache) + case None => + f.flatTap { calculatedValue => + checkFlagsAndPut(key, calculatedValue, ttl) + .handleError { e => + if (logger.isWarnEnabled) { + logger.warn(s"Failed to write to cache. Key = $key", e) + } } - } - }(_ => calculatedValue) - } - } + } + } } private def toKey(keyParts: Any*): String = diff --git a/modules/core/shared/src/main/scala/scalacache/Async.scala b/modules/core/shared/src/main/scala/scalacache/Async.scala deleted file mode 100644 index a890d13f..00000000 --- a/modules/core/shared/src/main/scala/scalacache/Async.scala +++ /dev/null @@ -1,133 +0,0 @@ -package scalacache - -import scala.concurrent.duration.Duration -import scala.concurrent.{Await, ExecutionContext, Future, Promise} -import scala.language.higherKinds -import scala.util.control.NonFatal -import scala.util.{Failure, Success, Try} - -trait MonadError[F[_]] { - - def pure[A](a: A): F[A] - - def map[A, B](fa: F[A])(f: A => B): F[B] - - def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] - - def raiseError[A](t: Throwable): F[A] - - // Note: the argument is by-name only for the sake of the Id instance. - // A bit hacky but it works, at the expense of some overhead for allocating a thunk - // even when other implementations don't need it. - def handleNonFatal[A](fa: => F[A])(f: Throwable => A): F[A] - -} - -trait Sync[F[_]] extends MonadError[F] { - - def delay[A](thunk: => A): F[A] - - def suspend[A](thunk: => F[A]): F[A] - -} - -trait Async[F[_]] extends Sync[F] { - - def async[A](register: (Either[Throwable, A] => Unit) => Unit): F[A] - -} - -object AsyncForId extends Async[Id] { - - def pure[A](a: A): Id[A] = a - - def map[A, B](fa: Id[A])(f: A => B): Id[B] = f(fa) - - def flatMap[A, B](fa: Id[A])(f: A => Id[B]): Id[B] = f(fa) - - def delay[A](thunk: => A): Id[A] = thunk - - def suspend[A](thunk: => Id[A]): Id[A] = thunk - - def raiseError[A](t: Throwable): Id[A] = throw t - - def handleNonFatal[A](fa: => Id[A])(f: Throwable => A): Id[A] = { - try { - fa - } catch { - case NonFatal(e) => f(e) - } - } - - def async[A](register: (Either[Throwable, A] => Unit) => Unit): Id[A] = { - val promise = Promise[A]() - register { - case Left(e) => promise.failure(e) - case Right(x) => promise.success(x) - } - Await.result(promise.future, Duration.Inf) - } - -} - -object AsyncForTry extends Async[Try] { - - def pure[A](a: A): Try[A] = Success(a) - - def map[A, B](fa: Try[A])(f: A => B): Try[B] = fa.map(f) - - def flatMap[A, B](fa: Try[A])(f: A => Try[B]): Try[B] = fa.flatMap(f) - - def delay[A](thunk: => A): Try[A] = Try(thunk) - - def suspend[A](thunk: => Try[A]): Try[A] = thunk - - def raiseError[A](t: Throwable): Try[A] = Failure(t) - - def async[A](register: (Either[Throwable, A] => Unit) => Unit): Try[A] = { - val promise = Promise[A]() - register { - case Left(e) => promise.failure(e) - case Right(x) => promise.success(x) - } - Try(Await.result(promise.future, Duration.Inf)) - } - - def handleNonFatal[A](fa: => Try[A])(f: Throwable => A): Try[A] = { - fa.recover { - case NonFatal(e) => f(e) - } - } - -} - -class AsyncForFuture(implicit ec: ExecutionContext) extends Async[Future] { - - def pure[A](a: A): Future[A] = Future.successful(a) - - def map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f) - - def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = fa.flatMap(f) - - def delay[A](thunk: => A): Future[A] = Future(thunk) - - def suspend[A](thunk: => Future[A]): Future[A] = thunk - - def raiseError[A](t: Throwable): Future[A] = Future.failed(t) - - def async[A](register: (Either[Throwable, A] => Unit) => Unit): Future[A] = { - val promise = Promise[A]() - register { - case Left(e) => promise.failure(e) - case Right(x) => promise.success(x) - } - promise.future - } - - def handleNonFatal[A](fa: => Future[A])(f: Throwable => A): Future[A] = { - fa.recover { - case NonFatal(e) => f(e) - } - } - -} diff --git a/modules/core/shared/src/main/scala/scalacache/Cache.scala b/modules/core/shared/src/main/scala/scalacache/Cache.scala index c6dc2a50..04d0f385 100644 --- a/modules/core/shared/src/main/scala/scalacache/Cache.scala +++ b/modules/core/shared/src/main/scala/scalacache/Cache.scala @@ -3,19 +3,20 @@ package scalacache import scala.concurrent.duration.Duration import scala.language.higherKinds -trait Cache[V] extends CacheAlg[V] { +//todo merge with alg? +trait Cache[F[_], V] extends CacheAlg[F, V] { def config: CacheConfig // Optimised methods for use by memoize: we know the key will be a single string so we can avoid some work. // These are public because calls to them are generated by the memoize macro. - def cachingForMemoize[F[_]](baseKey: String)(ttl: Option[Duration])( + def cachingForMemoize(baseKey: String)(ttl: Option[Duration])( f: => V - )(implicit mode: Mode[F], flags: Flags): F[V] + )(implicit flags: Flags): F[V] - def cachingForMemoizeF[F[_]](baseKey: String)(ttl: Option[Duration])( + def cachingForMemoizeF(baseKey: String)(ttl: Option[Duration])( f: => F[V] - )(implicit mode: Mode[F], flags: Flags): F[V] + )(implicit flags: Flags): F[V] } diff --git a/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala b/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala index 90f9f055..082567f9 100644 --- a/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala +++ b/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala @@ -7,20 +7,19 @@ import scala.language.higherKinds /** * Abstract algebra describing the operations a cache can perform * + * @tparam F the effect of the cache. //todo elaborate * @tparam V The value of types stored in the cache. */ -trait CacheAlg[V] { +trait CacheAlg[F[_], V] { /** * Get a value from the cache * * @param keyParts The cache key - * @param mode The operation mode, which decides the type of container in which to wrap the result * @param flags Flags used to conditionally alter the behaviour of ScalaCache - * @tparam F The type of container in which the result will be wrapped. This is decided by the mode. * @return The appropriate value, if it was found in the cache */ - def get[F[_]](keyParts: Any*)(implicit mode: Mode[F], flags: Flags): F[Option[V]] + def get(keyParts: Any*)(implicit flags: Flags): F[Option[V]] /** * Insert a value into the cache, optionally setting a TTL (time-to-live) @@ -28,27 +27,22 @@ trait CacheAlg[V] { * @param keyParts The cache key * @param value The value to insert * @param ttl The time-to-live. The cache entry will expire after this time has elapsed. - * @param mode The operation mode, which decides the type of container in which to wrap the result * @param flags Flags used to conditionally alter the behaviour of ScalaCache - * @tparam F The type of container in which the result will be wrapped. This is decided by the mode. */ - def put[F[_]](keyParts: Any*)(value: V, ttl: Option[Duration] = None)(implicit mode: Mode[F], flags: Flags): F[Any] + def put(keyParts: Any*)(value: V, ttl: Option[Duration] = None)(implicit flags: Flags): F[Any] /** * Remove the given key and its associated value from the cache, if it exists. * If the key is not in the cache, do nothing. * * @param keyParts data to be used to generate the cache key. This could be as simple as just a single String. See [[CacheKeyBuilder]]. - * @tparam F The type of container in which the result will be wrapped. This is decided by the mode. */ - def remove[F[_]](keyParts: Any*)(implicit mode: Mode[F]): F[Any] + def remove(keyParts: Any*): F[Any] /** * Delete the entire contents of the cache. Use wisely! - * - * @tparam F The type of container in which the result will be wrapped. This is decided by the mode. */ - def removeAll[F[_]]()(implicit mode: Mode[F]): F[Any] + def removeAll: F[Any] /** * Get a value from the cache if it exists. Otherwise compute it, insert it into the cache, and return it. @@ -56,12 +50,10 @@ trait CacheAlg[V] { * @param keyParts The cache key * @param ttl The time-to-live to use when inserting into the cache. The cache entry will expire after this time has elapsed. * @param f A block that computes the value - * @param mode The operation mode, which decides the type of container in which to wrap the result * @param flags Flags used to conditionally alter the behaviour of ScalaCache - * @tparam F The type of container in which the result will be wrapped. This is decided by the mode. * @return The value, either retrieved from the cache or computed */ - def caching[F[_]](keyParts: Any*)(ttl: Option[Duration])(f: => V)(implicit mode: Mode[F], flags: Flags): F[V] + def caching(keyParts: Any*)(ttl: Option[Duration])(f: => V)(implicit flags: Flags): F[V] /** * Get a value from the cache if it exists. Otherwise compute it, insert it into the cache, and return it. @@ -69,12 +61,10 @@ trait CacheAlg[V] { * @param keyParts The cache key * @param ttl The time-to-live to use when inserting into the cache. The cache entry will expire after this time has elapsed. * @param f A block that computes the value wrapped in a container - * @param mode The operation mode, which decides the type of container in which to wrap the result * @param flags Flags used to conditionally alter the behaviour of ScalaCache - * @tparam F The type of container in which the result will be wrapped. This is decided by the mode. * @return The value, either retrieved from the cache or computed */ - def cachingF[F[_]](keyParts: Any*)(ttl: Option[Duration])(f: => F[V])(implicit mode: Mode[F], flags: Flags): F[V] + def cachingF(keyParts: Any*)(ttl: Option[Duration])(f: F[V])(implicit flags: Flags): F[V] /** * You should call this when you have finished using this Cache. @@ -83,10 +73,8 @@ trait CacheAlg[V] { * It will take care of gracefully shutting down the underlying cache client. * * Note that you should not try to use this Cache instance after you have called this method. - * - * @param mode The operation mode, which decides the type of container in which to wrap the result - * @tparam F The type of container in which the result will be wrapped. This is decided by the mode. */ - def close[F[_]]()(implicit mode: Mode[F]): F[Any] + //TODO: Replace with Resource-based API? + def close: F[Any] } diff --git a/modules/core/shared/src/main/scala/scalacache/Mode.scala b/modules/core/shared/src/main/scala/scalacache/Mode.scala deleted file mode 100644 index a76fb47e..00000000 --- a/modules/core/shared/src/main/scala/scalacache/Mode.scala +++ /dev/null @@ -1,68 +0,0 @@ -package scalacache - -import scala.annotation.implicitNotFound -import scala.concurrent.{ExecutionContext, Future} -import scala.language.{higherKinds, implicitConversions} -import scala.util.Try - -/** - * When using ScalaCache you must import a mode in order to specify the effect monad - * in which you want to wrap your computations. - * - * @tparam F The effect monad that will wrap the return value of any cache operations. - * e.g. [[scalacache.Id]], [[scala.concurrent.Future]], [[scala.util.Try]] or cats-effect IO. - */ -@implicitNotFound(msg = """Could not find a Mode for type ${F}. - -If you want synchronous execution, try importing the sync mode: - -import scalacache.modes.sync._ - -If you are working with Scala Futures, import the scalaFuture mode -and don't forget you will also need an ExecutionContext: - -import scalacache.modes.scalaFuture._ -import scala.concurrent.ExecutionContext.Implicits.global - """) -trait Mode[F[_]] { - - def M: Async[F] - -} - -object modes { - - object sync { - - /** - * The simplest possible mode: just return the value as-is, without wrapping it in any effect monad. - */ - implicit val mode: Mode[Id] = new Mode[Id] { - val M: Async[Id] = AsyncForId - } - - } - - object try_ { - - /** - * A mode for wrapping synchronous cache operations in [[scala.util.Try]]. - */ - implicit val mode: Mode[Try] = new Mode[Try] { - val M: Async[Try] = AsyncForTry - } - - } - - object scalaFuture { - - /** - * A mode for wrapping asynchronous cache operations in [[scala.concurrent.Future]]. - */ - implicit def mode(implicit executionContext: ExecutionContext): Mode[Future] = - new Mode[Future] { - val M: Async[Future] = new AsyncForFuture - } - } - -} diff --git a/modules/core/shared/src/main/scala/scalacache/memoization/Macros.scala b/modules/core/shared/src/main/scala/scalacache/memoization/Macros.scala index 6cbb08f0..7d72e1bc 100644 --- a/modules/core/shared/src/main/scala/scalacache/memoization/Macros.scala +++ b/modules/core/shared/src/main/scala/scalacache/memoization/Macros.scala @@ -4,37 +4,29 @@ import scala.language.experimental.macros import scala.reflect.macros.blackbox import scala.concurrent.duration.Duration import scala.language.higherKinds -import scalacache.{Flags, Cache, Mode} +import scalacache.{Flags, Cache} class Macros(val c: blackbox.Context) { import c.universe._ def memoizeImpl[F[_], V: c.WeakTypeTag]( ttl: c.Expr[Option[Duration]] - )(f: c.Tree)(cache: c.Expr[Cache[V]], mode: c.Expr[Mode[F]], flags: c.Expr[Flags]): c.Tree = { + )(f: c.Tree)(cache: c.Expr[Cache[F, V]], flags: c.Expr[Flags]): c.Tree = { commonMacroImpl(cache, { keyName => - q"""$cache.cachingForMemoize($keyName)($ttl)($f)($mode, $flags)""" + q"""$cache.cachingForMemoize($keyName)($ttl)($f)($flags)""" }) } def memoizeFImpl[F[_], V: c.WeakTypeTag]( ttl: c.Expr[Option[Duration]] - )(f: c.Tree)(cache: c.Expr[Cache[V]], mode: c.Expr[Mode[F]], flags: c.Expr[Flags]): c.Tree = { + )(f: c.Tree)(cache: c.Expr[Cache[F, V]], flags: c.Expr[Flags]): c.Tree = { commonMacroImpl(cache, { keyName => - q"""$cache.cachingForMemoizeF($keyName)($ttl)($f)($mode, $flags)""" - }) - } - - def memoizeSyncImpl[V: c.WeakTypeTag]( - ttl: c.Expr[Option[Duration]] - )(f: c.Tree)(cache: c.Expr[Cache[V]], mode: c.Expr[Mode[scalacache.Id]], flags: c.Expr[Flags]): c.Tree = { - commonMacroImpl(cache, { keyName => - q"""$cache.cachingForMemoize($keyName)($ttl)($f)($mode, $flags)""" + q"""$cache.cachingForMemoizeF($keyName)($ttl)($f)($flags)""" }) } private def commonMacroImpl[F[_], V: c.WeakTypeTag]( - cache: c.Expr[Cache[V]], + cache: c.Expr[Cache[F, V]], keyNameToCachingCall: (c.TermName) => c.Tree ): Tree = { diff --git a/modules/core/shared/src/main/scala/scalacache/memoization/package.scala b/modules/core/shared/src/main/scala/scalacache/memoization/package.scala index 3e4cd5b1..5ddfa0ec 100644 --- a/modules/core/shared/src/main/scala/scalacache/memoization/package.scala +++ b/modules/core/shared/src/main/scala/scalacache/memoization/package.scala @@ -30,7 +30,7 @@ package object memoization { * @tparam V The type of the value to be cached * @return A result, either retrieved from the cache or calculated by executing the function `f` */ - def memoize[F[_], V](ttl: Option[Duration])(f: => V)(implicit cache: Cache[V], mode: Mode[F], flags: Flags): F[V] = + def memoize[F[_], V](ttl: Option[Duration])(f: => V)(implicit cache: Cache[F, V], flags: Flags): F[V] = macro Macros.memoizeImpl[F, V] /** @@ -54,31 +54,6 @@ package object memoization { */ def memoizeF[F[_], V]( ttl: Option[Duration] - )(f: => F[V])(implicit cache: Cache[V], mode: Mode[F], flags: Flags): F[V] = + )(f: => F[V])(implicit cache: Cache[F, V], flags: Flags): F[V] = macro Macros.memoizeFImpl[F, V] - - /** - * A version of [[memoize]] that is specialised to [[Id]]. - * This is provided for convenience because type inference doesn't work properly for [[Id]], - * and writing `memoize[Id, Foo]` is a bit rubbish. - * - * Perform the given operation and memoize its result to a cache before returning it. - * If the result is already in the cache, return it without performing the operation. - * - * If a TTL is given, the result is stored in the cache with that TTL. - * It will be evicted when the TTL is up. - * - * Note that if the result is currently in the cache, changing the TTL has no effect. - * TTL is only set once, when the result is added to the cache. - * - * @param ttl Time-To-Live - * @param f A function that computes some result. This result is the value that will be cached. - * @param cache The cache - * @param flags Flags used to conditionally alter the behaviour of ScalaCache - * @tparam V The type of the value to be cached - * @return A result, either retrieved from the cache or calculated by executing the function `f` - */ - def memoizeSync[V](ttl: Option[Duration])(f: => V)(implicit cache: Cache[V], mode: Mode[Id], flags: Flags): V = - macro Macros.memoizeSyncImpl[V] - } diff --git a/modules/core/shared/src/main/scala/scalacache/package.scala b/modules/core/shared/src/main/scala/scalacache/package.scala index 0cc31ad7..93ac8d33 100644 --- a/modules/core/shared/src/main/scala/scalacache/package.scala +++ b/modules/core/shared/src/main/scala/scalacache/package.scala @@ -3,6 +3,7 @@ import scala.language.higherKinds package object scalacache { + //todo type Id[X] = X /** @@ -18,7 +19,7 @@ package object scalacache { * @tparam V The type of the corresponding value * @return the value, if there is one */ - def get[F[_], V](keyParts: Any*)(implicit cache: Cache[V], mode: Mode[F], flags: Flags): F[Option[V]] = + def get[F[_], V](keyParts: Any*)(implicit cache: Cache[F, V], flags: Flags): F[Option[V]] = cache.get(keyParts: _*) /** @@ -39,7 +40,7 @@ package object scalacache { */ def put[F[_], V]( keyParts: Any* - )(value: V, ttl: Option[Duration] = None)(implicit cache: Cache[V], mode: Mode[F], flags: Flags): F[Any] = + )(value: V, ttl: Option[Duration] = None)(implicit cache: Cache[F, V], flags: Flags): F[Any] = cache.put(keyParts: _*)(value, ttl) /** @@ -56,11 +57,12 @@ package object scalacache { * @tparam F The type of container in which the result will be wrapped. This is decided by the mode. * @tparam V The type of the value to be removed */ - def remove[F[_], V](keyParts: Any*)(implicit cache: Cache[V], mode: Mode[F]): F[Any] = + def remove[F[_], V](keyParts: Any*)(implicit cache: Cache[F, V]): F[Any] = cache.remove(keyParts: _*) + //todo merge with `removeAll`? final class RemoveAll[V] { - def apply[F[_]]()(implicit cache: Cache[V], mode: Mode[F]): F[Any] = cache.removeAll[F]() + def apply[F[_]]()(implicit cache: Cache[F, V]): F[Any] = cache.removeAll } /** @@ -92,7 +94,7 @@ package object scalacache { */ def caching[F[_], V]( keyParts: Any* - )(ttl: Option[Duration])(f: => V)(implicit cache: Cache[V], mode: Mode[F], flags: Flags): F[V] = + )(ttl: Option[Duration])(f: => V)(implicit cache: Cache[F, V], flags: Flags): F[V] = cache.caching(keyParts: _*)(ttl)(f) /** @@ -117,47 +119,6 @@ package object scalacache { */ def cachingF[F[_], V]( keyParts: Any* - )(ttl: Option[Duration])(f: => F[V])(implicit cache: Cache[V], mode: Mode[F], flags: Flags): F[V] = + )(ttl: Option[Duration])(f: => F[V])(implicit cache: Cache[F, V], flags: Flags): F[V] = cache.cachingF(keyParts: _*)(ttl)(f) - - /** - * A version of the API that is specialised to [[Id]]. - * The functions in this API perform their operations immediately - * on the current thread and thus do not wrap their results in any effect monad. - * - * === - * - * Implementation note: I really didn't want to have this separate copy of the API, - * but I couldn't get type inference to understand that Id[A] == A. - * e.g. the following doesn't compile: - * - * implicit val cache: LovelyCache[String] = ??? - * import scalacache.modes.sync._ - * val x: Option[String] = scalacache.get("hello") - * - * [error] ... polymorphic expression cannot be instantiated to expected type; - * [error] found : [F[_], V]F[Option[V]] - * [error] required: Option[String] - * - * If anyone can find a workaround to make this compile, I will be much obliged. - */ - object sync { - - def get[V](keyParts: Any*)(implicit cache: Cache[V], mode: Mode[Id], flags: Flags): Option[V] = - cache.get[Id](keyParts: _*) - - def put[V]( - keyParts: Any* - )(value: V, ttl: Option[Duration] = None)(implicit cache: Cache[V], mode: Mode[Id], flags: Flags): Any = - cache.put[Id](keyParts: _*)(value, ttl) - - def remove[V](keyParts: Any*)(implicit cache: Cache[V], mode: Mode[Id]): Any = - cache.remove[Id](keyParts: _*) - - def caching[V]( - keyParts: Any* - )(ttl: Option[Duration])(f: => V)(implicit cache: Cache[V], mode: Mode[Id], flags: Flags): V = - cache.caching[Id](keyParts: _*)(ttl)(f) - } - } diff --git a/modules/docs/src/main/mdoc/docs/modes.md b/modules/docs/src/main/mdoc/docs/modes.md deleted file mode 100644 index 9e1ab0d8..00000000 --- a/modules/docs/src/main/mdoc/docs/modes.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -layout: docs -title: Modes ---- - -### Modes - -Depending on your application, you might want ScalaCache to wrap its operations in a Try, a Future, a Scalaz Task, or some other effect container. - -Or maybe you want to keep it simple and just return plain old values, performing the operations on the current thread and throwing exceptions in case of failure. - -In order to control ScalaCache's behaviour in this way, you need to choose a "mode". - -ScalaCache comes with a few built-in modes. - -#### Synchronous mode - -```scala mdoc:silent -import scalacache.modes.sync._ -``` - -* Blocks the current thread until the operation completes -* Returns a plain value, not wrapped in any container -* Throws exceptions in case of failure - -Note: If you're using an in-memory cache (e.g. Caffeine) then it makes sense to use the synchronous mode. But if you're communicating with a cache over a network (e.g. Redis, Memcached) then this mode is not recommended. If the network goes down, your app could hang forever! - -#### Try mode - -```scala mdoc:silent -import scalacache.modes.try_._ -``` - -* Blocks the current thread until the operation completes -* Wraps failures in `scala.util.Failure` - -#### Future mode - -```scala mdoc:silent -import scalacache.modes.scalaFuture._ -``` - -* Executes the operation on a separate thread and returns a `scala.concurrent.Future` - -You will also need an ExecutionContext in implicit scope: - -```scala mdoc:silent -import scala.concurrent.ExecutionContext.Implicits.global -``` - -#### cats-effect IO mode - -You will need a dependency on the `scalacache-cats-effect` module: - -``` -libraryDependencies += "com.github.cb372" %% "scalacache-cats-effect" % "0.28.0" -``` - -```scala mdoc:silent -import scalacache.Mode -import cats.effect.IO -implicit val mode: Mode[IO] = scalacache.CatsEffect.modes.async -``` - -* Wraps the operation in `IO`, deferring execution until it is explicitly run - -#### Monix Task (Monix 3.x) - -You will need a dependency on the `scalacache-cats-effect` module: - -``` -libraryDependencies += "com.github.cb372" %% "scalacache-cats-effect" % "0.28.0" -``` - -```scala -import monix.eval.Task -implicit val mode: Mode[Task] = scalacache.CatsEffect.modes.async -``` - -* Wraps the operation in Monix `Task`, deferring execution until it is explicitly run - -Note: There used to a `scalacache-monix` module but it was removed because it -didn't do very much. - -#### Scalaz Task - -You will need a dependency on the `scalacache-scalaz72` module: - -``` -libraryDependencies += "com.github.cb372" %% "scalacache-scalaz72" % "0.28.0" -``` - -```scala mdoc:silent -import scalacache.Scalaz72.modes._ -``` - -* Wraps the operation in `Task`, deferring execution until it is explicitly run - diff --git a/modules/docs/src/main/mdoc/docs/sync-api.md b/modules/docs/src/main/mdoc/docs/sync-api.md deleted file mode 100644 index c42d0c62..00000000 --- a/modules/docs/src/main/mdoc/docs/sync-api.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -layout: docs -title: Synchronous API ---- - -### Synchronous API - -Unfortunately Scala's type inference doesn't play very nicely with ScalaCache's synchronous mode. - -If you want to use synchronous mode, the synchronous API is recommended. This works in exactly the same way as the normal API; it is provided merely as a convenience so you don't have to jump through hoops to make your code compile when using the synchronous mode. - -```scala mdoc:reset-object -import scalacache._ -import scalacache.memcached._ -import scalacache.modes.sync._ - -import scalacache.serialization.binary._ - -final case class Cat(id: Int, name: String, colour: String) - -implicit val catsCache: Cache[Cat] = MemcachedCache("localhost:11211") - -val myValue: Option[Cat] = sync.get("eric") -val ericTheCat = Cat(1, "Eric", "tuxedo") -``` - -There is also a synchronous version of `caching`: - -```scala mdoc -val result = sync.caching("myKey")(ttl = None) { - // do stuff... - ericTheCat -} -``` - -```scala mdoc:invisible -for (cache <- List(catsCache)) { - cache.close()(scalacache.modes.sync.mode) -} -``` From 30dafe52ed6026b3aeba3c1c73c45204621acafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Fri, 7 Feb 2020 02:29:33 +0100 Subject: [PATCH 02/21] Port caffeine module --- .../scalacache/caffeine/CaffeineCache.scala | 72 ++++++++++--------- .../src/main/scala/scalacache/Entry.scala | 14 ++-- 2 files changed, 49 insertions(+), 37 deletions(-) diff --git a/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala b/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala index 05bc8e22..a821c66a 100644 --- a/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala +++ b/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala @@ -1,79 +1,87 @@ package scalacache.caffeine import java.time.temporal.ChronoUnit -import java.time.{Clock, Instant} +import java.time.{Instant} import com.github.benmanes.caffeine.cache.{Caffeine, Cache => CCache} - +import cats.effect.Clock import scalacache.logging.Logger -import scalacache.{AbstractCache, CacheConfig, Entry, Mode} +import scalacache.{AbstractCache, CacheConfig, Entry} import scala.concurrent.duration.Duration import scala.language.higherKinds +import cats.effect.Sync +import java.util.concurrent.TimeUnit +import cats.implicits._ +import cats.MonadError +import cats.Defer /* * Thin wrapper around Caffeine. * * This cache implementation is synchronous. */ -class CaffeineCache[V](val underlying: CCache[String, Entry[V]])( +class CaffeineCache[F[_], V](val underlying: CCache[String, Entry[F, V]])( implicit val config: CacheConfig, - clock: Clock = Clock.systemUTC() -) extends AbstractCache[V] { + clock: Clock[F], + val F: Sync[F] +) extends AbstractCache[F, V] { + protected implicit val Defer: Defer[F] = F override protected final val logger = Logger.getLogger(getClass.getName) - def doGet[F[_]](key: String)(implicit mode: Mode[F]): F[Option[V]] = { - mode.M.delay { - val entry = underlying.getIfPresent(key) - val result = { - if (entry == null || entry.isExpired) - None - else - Some(entry.value) + def doGet(key: String): F[Option[V]] = { + F.delay { + Option(underlying.getIfPresent(key)) + } + .flatMap(_.filterA(_.isExpired)) + .map(_.map(_.value)) + .flatTap { result => + F.delay { + logCacheHitOrMiss(key, result) + } } - logCacheHitOrMiss(key, result) - result - } } - def doPut[F[_]](key: String, value: V, ttl: Option[Duration])(implicit mode: Mode[F]): F[Any] = { - mode.M.delay { - val entry = Entry(value, ttl.map(toExpiryTime)) + def doPut(key: String, value: V, ttl: Option[Duration]): F[Any] = ttl.traverse(toExpiryTime).flatMap { expiry => + F.delay { + val entry = Entry[F, V](value, expiry) underlying.put(key, entry) logCachePut(key, ttl) } } - override def doRemove[F[_]](key: String)(implicit mode: Mode[F]): F[Any] = - mode.M.delay(underlying.invalidate(key)) + override def doRemove(key: String): F[Any] = + F.delay(underlying.invalidate(key)) - override def doRemoveAll[F[_]]()(implicit mode: Mode[F]): F[Any] = - mode.M.delay(underlying.invalidateAll()) + override def doRemoveAll(): F[Any] = + F.delay(underlying.invalidateAll()) - override def close[F[_]]()(implicit mode: Mode[F]): F[Any] = { + override def close: F[Any] = { // Nothing to do - mode.M.pure(()) + F.pure(()) } - private def toExpiryTime(ttl: Duration): Instant = - Instant.now(clock).plus(ttl.toMillis, ChronoUnit.MILLIS) + private def toExpiryTime(ttl: Duration): F[Instant] = + clock.monotonic(TimeUnit.MILLISECONDS).map(Instant.ofEpochMilli(_).plusMillis(ttl.toMillis)) } object CaffeineCache { /** - * Create a new Caffeine cache + * Create a new Caffeine cache. */ - def apply[V](implicit config: CacheConfig): CaffeineCache[V] = - apply(Caffeine.newBuilder().build[String, Entry[V]]()) + def apply[F[_]: Sync: Clock, V](implicit config: CacheConfig): F[CaffeineCache[F, V]] = + Sync[F].delay(Caffeine.newBuilder().build[String, Entry[F, V]]()).map(apply(_)) /** * Create a new cache utilizing the given underlying Caffeine cache. * * @param underlying a Caffeine cache */ - def apply[V](underlying: CCache[String, Entry[V]])(implicit config: CacheConfig): CaffeineCache[V] = + def apply[F[_]: Sync: Clock, V]( + underlying: CCache[String, Entry[F, V]] + )(implicit config: CacheConfig): CaffeineCache[F, V] = new CaffeineCache(underlying) } diff --git a/modules/core/shared/src/main/scala/scalacache/Entry.scala b/modules/core/shared/src/main/scala/scalacache/Entry.scala index 7fecb14a..0ad4315e 100644 --- a/modules/core/shared/src/main/scala/scalacache/Entry.scala +++ b/modules/core/shared/src/main/scala/scalacache/Entry.scala @@ -1,16 +1,20 @@ package scalacache -import java.time.{Clock, Instant} +import java.time.{Instant} +import cats.effect.Clock +import java.util.concurrent.TimeUnit +import cats.implicits._ +import language.higherKinds +import cats.Functor /** * A cache entry with an optional expiry time */ -case class Entry[+A](value: A, expiresAt: Option[Instant]) { +case class Entry[F[_], +A](value: A, expiresAt: Option[Instant]) { /** * Has the entry expired yet? */ - def isExpired(implicit clock: Clock): Boolean = - expiresAt.exists(_.isBefore(Instant.now(clock))) - + def isExpired(implicit clock: Clock[F], functor: Functor[F]): F[Boolean] = + clock.monotonic(TimeUnit.MILLISECONDS).map(Instant.ofEpochMilli(_)).map(now => expiresAt.exists(_.isBefore(now))) } From 4505187335f2e4cf03ac2ae5eed96a106277bc23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Fri, 7 Feb 2020 02:36:27 +0100 Subject: [PATCH 03/21] It's better if it's traverse! (no, really) By traversing on the entry's TTL we get the opportunity to avoid calculating the time in case there's no TTL. That might give us some extra performance in case of infinitely long-lived keys. The constraint is more powerful, but all the caching implementations will be at least monads, so it's fine. And Clock is only possible to implement in a real scenario for instances of Sync. --- .../shared/src/main/scala/scalacache/Entry.scala | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/core/shared/src/main/scala/scalacache/Entry.scala b/modules/core/shared/src/main/scala/scalacache/Entry.scala index 0ad4315e..f153872b 100644 --- a/modules/core/shared/src/main/scala/scalacache/Entry.scala +++ b/modules/core/shared/src/main/scala/scalacache/Entry.scala @@ -1,11 +1,11 @@ package scalacache -import java.time.{Instant} +import java.time.Instant import cats.effect.Clock import java.util.concurrent.TimeUnit import cats.implicits._ import language.higherKinds -import cats.Functor +import cats.Applicative /** * A cache entry with an optional expiry time @@ -15,6 +15,10 @@ case class Entry[F[_], +A](value: A, expiresAt: Option[Instant]) { /** * Has the entry expired yet? */ - def isExpired(implicit clock: Clock[F], functor: Functor[F]): F[Boolean] = - clock.monotonic(TimeUnit.MILLISECONDS).map(Instant.ofEpochMilli(_)).map(now => expiresAt.exists(_.isBefore(now))) + def isExpired(implicit clock: Clock[F], applicative: Applicative[F]): F[Boolean] = + expiresAt + .traverse { ttl => + clock.monotonic(TimeUnit.MILLISECONDS).map(Instant.ofEpochMilli(_)).map(ttl.isBefore(_)) + } + .map(_.contains(true)) } From b88d5cb24dc20c4e58fe264b23ac98bed0bcb896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 22 Mar 2020 19:13:48 +0100 Subject: [PATCH 04/21] Remove ohc again --- build.sbt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/build.sbt b/build.sbt index 5b80ba5c..d855766b 100644 --- a/build.sbt +++ b/build.sbt @@ -30,7 +30,6 @@ lazy val root: Project = Project(id = "scalacache", base = file(".")) redis, caffeine, catsEffect, - ohc, scalaz72, circe, tests @@ -107,13 +106,6 @@ lazy val catsEffect = jvmOnlyModule("cats-effect") coverageFailOnMinimum := true ) -lazy val ohc = jvmOnlyModule("ohc") - .settings( - libraryDependencies ++= Seq( - "org.caffinitas.ohc" % "ohc-core" % "0.7.0" - ) - ) - lazy val scalaz72 = jvmOnlyModule("scalaz72") .settings( libraryDependencies ++= Seq( @@ -145,7 +137,7 @@ lazy val circe = jvmOnlyModule("circe") lazy val tests = jvmOnlyModule("tests") .settings(publishArtifact := false) - .dependsOn(cache2k, caffeine, memcached, redis, ohc, scalaz72, circe, catsEffect) + .dependsOn(caffeine, memcached, redis, scalaz72, circe, catsEffect) lazy val docs = jvmOnlyModule("docs") .enablePlugins(MicrositesPlugin) @@ -170,7 +162,6 @@ lazy val docs = jvmOnlyModule("docs") redis, caffeine, catsEffect, - ohc, scalaz72, circe ) From 3d5438b29d941d1ecd00d7c39b2f17fbb70714e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 22 Mar 2020 19:14:20 +0100 Subject: [PATCH 05/21] Restore line --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index d855766b..ddf20a18 100644 --- a/build.sbt +++ b/build.sbt @@ -137,7 +137,7 @@ lazy val circe = jvmOnlyModule("circe") lazy val tests = jvmOnlyModule("tests") .settings(publishArtifact := false) - .dependsOn(caffeine, memcached, redis, scalaz72, circe, catsEffect) + .dependsOn(caffeine, memcached, redis, catsEffect, scalaz72, circe) lazy val docs = jvmOnlyModule("docs") .enablePlugins(MicrositesPlugin) From 19495b428614cfd743dfb524dace4e28dfaa80e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 22 Mar 2020 19:14:27 +0100 Subject: [PATCH 06/21] Update cats-effect to 2.1.2 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index ddf20a18..263eacd3 100644 --- a/build.sbt +++ b/build.sbt @@ -42,7 +42,7 @@ lazy val core = moduleName := "scalacache-core", libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value, - "org.typelevel" %% "cats-effect" % "2.1.0", + "org.typelevel" %% "cats-effect" % "2.1.2", "org.scalatest" %%% "scalatest" % "3.0.8" % Test, "org.scalacheck" %%% "scalacheck" % "1.14.3" % Test ), From 51f84e3828066904405a42570542dc7bc5ba108a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 22 Mar 2020 19:51:32 +0100 Subject: [PATCH 07/21] Add Sync requirement to abstract cache, compile core tests --- .../main/scala/scalacache/AbstractCache.scala | 21 ++- .../src/main/scala/scalacache/Cache.scala | 2 +- .../src/main/scala/scalacache/CacheAlg.scala | 8 +- .../scalacache/memoization/package.scala | 2 +- .../src/main/scala/scalacache/package.scala | 7 +- .../src/test/scala/issue42/Issue42Spec.scala | 18 ++- .../shared/src/test/scala/sample/Sample.scala | 22 ++- .../scala/scalacache/AbstractCacheSpec.scala | 147 ++++++++++-------- ...cheKeyExcludingConstructorParamsSpec.scala | 27 ++-- ...cheKeyIncludingConstructorParamsSpec.scala | 27 ++-- .../memoization/CacheKeySpecCommon.scala | 54 +++---- .../scalacache/memoization/MemoizeSpec.scala | 71 ++++----- .../scalacache/memoization/pkg/package.scala | 8 +- .../src/test/scala/scalacache/mocks.scala | 102 ++++++------ 14 files changed, 273 insertions(+), 243 deletions(-) diff --git a/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala b/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala index eb5ebccb..906c0434 100644 --- a/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala +++ b/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala @@ -6,7 +6,7 @@ import scala.language.higherKinds import cats.Monad import cats.implicits._ import cats.MonadError -import cats.Defer +import cats.effect.Sync /** * An abstract implementation of [[CacheAlg]] that takes care of @@ -19,8 +19,7 @@ import cats.Defer */ trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { - protected implicit def F: MonadError[F, Throwable] - protected implicit def Defer: Defer[F] + protected implicit def F: Sync[F] // GET protected def doGet(key: String): F[Option[V]] @@ -43,12 +42,12 @@ trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { // PUT - protected def doPut(key: String, value: V, ttl: Option[Duration]): F[Any] + protected def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] private def checkFlagsAndPut(key: String, value: V, ttl: Option[Duration])( implicit flags: Flags - ): F[Any] = { + ): F[Unit] = { if (flags.writesEnabled) { doPut(key, value, ttl) } else { @@ -61,7 +60,7 @@ trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { final override def put( keyParts: Any* - )(value: V, ttl: Option[Duration])(implicit flags: Flags): F[Any] = { + )(value: V, ttl: Option[Duration])(implicit flags: Flags): F[Unit] = { val key = toKey(keyParts: _*) val finiteTtl = ttl.filter(_.isFinite) // discard Duration.Inf, Duration.Undefined checkFlagsAndPut(key, value, finiteTtl) @@ -69,16 +68,16 @@ trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { // REMOVE - protected def doRemove(key: String): F[Any] + protected def doRemove(key: String): F[Unit] - final override def remove(keyParts: Any*): F[Any] = + final override def remove(keyParts: Any*): F[Unit] = doRemove(toKey(keyParts: _*)) // REMOVE ALL - protected def doRemoveAll: F[Any] + protected def doRemoveAll: F[Unit] - final override def removeAll: F[Any] = + final override def removeAll: F[Unit] = doRemoveAll // CACHING @@ -116,7 +115,7 @@ trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { private def _caching(key: String, ttl: Option[Duration], f: => V)( implicit flags: Flags - ): F[V] = _cachingF(key, ttl, Defer.defer(F.pure(f))) + ): F[V] = _cachingF(key, ttl, Sync[F].delay(f)) private def _cachingF(key: String, ttl: Option[Duration], f: => F[V])( implicit diff --git a/modules/core/shared/src/main/scala/scalacache/Cache.scala b/modules/core/shared/src/main/scala/scalacache/Cache.scala index 04d0f385..735cf1e4 100644 --- a/modules/core/shared/src/main/scala/scalacache/Cache.scala +++ b/modules/core/shared/src/main/scala/scalacache/Cache.scala @@ -16,7 +16,7 @@ trait Cache[F[_], V] extends CacheAlg[F, V] { )(implicit flags: Flags): F[V] def cachingForMemoizeF(baseKey: String)(ttl: Option[Duration])( - f: => F[V] + f: F[V] )(implicit flags: Flags): F[V] } diff --git a/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala b/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala index 082567f9..282ae40a 100644 --- a/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala +++ b/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala @@ -29,7 +29,7 @@ trait CacheAlg[F[_], V] { * @param ttl The time-to-live. The cache entry will expire after this time has elapsed. * @param flags Flags used to conditionally alter the behaviour of ScalaCache */ - def put(keyParts: Any*)(value: V, ttl: Option[Duration] = None)(implicit flags: Flags): F[Any] + def put(keyParts: Any*)(value: V, ttl: Option[Duration] = None)(implicit flags: Flags): F[Unit] /** * Remove the given key and its associated value from the cache, if it exists. @@ -37,12 +37,12 @@ trait CacheAlg[F[_], V] { * * @param keyParts data to be used to generate the cache key. This could be as simple as just a single String. See [[CacheKeyBuilder]]. */ - def remove(keyParts: Any*): F[Any] + def remove(keyParts: Any*): F[Unit] /** * Delete the entire contents of the cache. Use wisely! */ - def removeAll: F[Any] + def removeAll: F[Unit] /** * Get a value from the cache if it exists. Otherwise compute it, insert it into the cache, and return it. @@ -75,6 +75,6 @@ trait CacheAlg[F[_], V] { * Note that you should not try to use this Cache instance after you have called this method. */ //TODO: Replace with Resource-based API? - def close: F[Any] + def close: F[Unit] } diff --git a/modules/core/shared/src/main/scala/scalacache/memoization/package.scala b/modules/core/shared/src/main/scala/scalacache/memoization/package.scala index 5ddfa0ec..263a7684 100644 --- a/modules/core/shared/src/main/scala/scalacache/memoization/package.scala +++ b/modules/core/shared/src/main/scala/scalacache/memoization/package.scala @@ -54,6 +54,6 @@ package object memoization { */ def memoizeF[F[_], V]( ttl: Option[Duration] - )(f: => F[V])(implicit cache: Cache[F, V], flags: Flags): F[V] = + )(f: F[V])(implicit cache: Cache[F, V], flags: Flags): F[V] = macro Macros.memoizeFImpl[F, V] } diff --git a/modules/core/shared/src/main/scala/scalacache/package.scala b/modules/core/shared/src/main/scala/scalacache/package.scala index 93ac8d33..a8a234d2 100644 --- a/modules/core/shared/src/main/scala/scalacache/package.scala +++ b/modules/core/shared/src/main/scala/scalacache/package.scala @@ -40,7 +40,7 @@ package object scalacache { */ def put[F[_], V]( keyParts: Any* - )(value: V, ttl: Option[Duration] = None)(implicit cache: Cache[F, V], flags: Flags): F[Any] = + )(value: V, ttl: Option[Duration] = None)(implicit cache: Cache[F, V], flags: Flags): F[Unit] = cache.put(keyParts: _*)(value, ttl) /** @@ -57,12 +57,11 @@ package object scalacache { * @tparam F The type of container in which the result will be wrapped. This is decided by the mode. * @tparam V The type of the value to be removed */ - def remove[F[_], V](keyParts: Any*)(implicit cache: Cache[F, V]): F[Any] = + def remove[F[_], V](keyParts: Any*)(implicit cache: Cache[F, V]): F[Unit] = cache.remove(keyParts: _*) - //todo merge with `removeAll`? final class RemoveAll[V] { - def apply[F[_]]()(implicit cache: Cache[F, V]): F[Any] = cache.removeAll + def apply[F[_]]()(implicit cache: Cache[F, V]): F[Unit] = cache.removeAll } /** diff --git a/modules/core/shared/src/test/scala/issue42/Issue42Spec.scala b/modules/core/shared/src/test/scala/issue42/Issue42Spec.scala index 018b42cd..c470a4a1 100644 --- a/modules/core/shared/src/test/scala/issue42/Issue42Spec.scala +++ b/modules/core/shared/src/test/scala/issue42/Issue42Spec.scala @@ -3,6 +3,7 @@ package issue42 import org.scalatest.{FlatSpec, Matchers} import scala.util.Random +import cats.effect.SyncIO class Issue42Spec extends FlatSpec with Matchers { @@ -14,18 +15,19 @@ class Issue42Spec extends FlatSpec with Matchers { import concurrent.duration._ import scala.language.postfixOps - implicit val cache: Cache[User] = new MockCache() - import scalacache.modes.sync._ + implicit val cache: Cache[SyncIO, User] = new MockCache() def generateNewName() = Random.alphanumeric.take(10).mkString - def getUser(id: Int)(implicit flags: Flags): User = memoizeSync(None) { - User(id, generateNewName()) - } + def getUser(id: Int)(implicit flags: Flags): User = + memoize(None) { + User(id, generateNewName()) + }.unsafeRunSync() - def getUserWithTtl(id: Int)(implicit flags: Flags): User = memoizeSync(Some(1 days)) { - User(id, generateNewName()) - } + def getUserWithTtl(id: Int)(implicit flags: Flags): User = + memoize(Some(1 days)) { + User(id, generateNewName()) + }.unsafeRunSync() "memoize without TTL" should "respect implicit flags" in { val user1before = getUser(1) diff --git a/modules/core/shared/src/test/scala/sample/Sample.scala b/modules/core/shared/src/test/scala/sample/Sample.scala index 4e026070..2a608d03 100644 --- a/modules/core/shared/src/test/scala/sample/Sample.scala +++ b/modules/core/shared/src/test/scala/sample/Sample.scala @@ -2,13 +2,11 @@ package sample import scalacache._ import memoization._ -import scalacache.modes.scalaFuture._ -import scala.concurrent.Future import scala.concurrent.duration._ -import scala.concurrent.ExecutionContext.Implicits.global import language.postfixOps +import cats.effect.IO case class User(id: Int, name: String) @@ -18,24 +16,24 @@ case class User(id: Int, name: String) object Sample extends App { class UserRepository { - implicit val cache: Cache[User] = new MockCache() + implicit val cache: Cache[IO, User] = new MockCache() - def getUser(id: Int): Future[User] = memoizeF(None) { + def getUser(id: Int): IO[User] = memoizeF(None) { // Do DB lookup here... - Future { User(id, s"user$id") } + IO { User(id, s"user$id") } } - def withExpiry(id: Int): Future[User] = memoizeF(Some(60 seconds)) { + def withExpiry(id: Int): IO[User] = memoizeF(Some(60 seconds)) { // Do DB lookup here... - Future { User(id, s"user$id") } + IO { User(id, s"user$id") } } - def withOptionalExpiry(id: Int): Future[User] = memoizeF(Some(60 seconds)) { - Future { User(id, s"user$id") } + def withOptionalExpiry(id: Int): IO[User] = memoizeF(Some(60 seconds)) { + IO { User(id, s"user$id") } } - def withOptionalExpiryNone(id: Int): Future[User] = memoizeF(None) { - Future { User(id, s"user$id") } + def withOptionalExpiryNone(id: Int): IO[User] = memoizeF(None) { + IO { User(id, s"user$id") } } } diff --git a/modules/core/shared/src/test/scala/scalacache/AbstractCacheSpec.scala b/modules/core/shared/src/test/scala/scalacache/AbstractCacheSpec.scala index a6f550ea..d9203e72 100644 --- a/modules/core/shared/src/test/scala/scalacache/AbstractCacheSpec.scala +++ b/modules/core/shared/src/test/scala/scalacache/AbstractCacheSpec.scala @@ -4,41 +4,42 @@ import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers} import scala.concurrent.duration._ import scala.language.postfixOps -import scalacache.modes.sync._ import scala.util.{Success, Try} +import cats.effect.SyncIO +import cats.implicits._ class AbstractCacheSpec extends FlatSpec with Matchers with BeforeAndAfter { - val cache = new LoggingMockCache[String] + val cache = new LoggingMockCache[SyncIO, String] before { cache.mmap.clear() - cache.reset() + cache.reset.unsafeRunSync() } behavior of "#get" it should "call doGet on the concrete cache" in { - cache.get("foo") + cache.get("foo").unsafeRunSync() cache.getCalledWithArgs(0) should be("foo") } it should "use the CacheKeyBuilder to build the cache key" in { - cache.get("foo", 123) + cache.get("foo", 123).unsafeRunSync() cache.getCalledWithArgs(0) should be("foo:123") } it should "not call doGet on the concrete cache if cache reads are disabled" in { implicit val flags: Flags = Flags(readsEnabled = false) - cache.get("foo") + cache.get("foo").unsafeRunSync() cache.getCalledWithArgs should be(empty) } it should "conditionally call doGet on the concrete cache depending on the readsEnabled flag" in { def possiblyGetFromCache(key: String): Unit = { implicit def flags: Flags = Flags(readsEnabled = (key == "foo")) - cache.get(key) + cache.get(key).unsafeRunSync() } possiblyGetFromCache("foo") possiblyGetFromCache("bar") @@ -49,41 +50,43 @@ class AbstractCacheSpec extends FlatSpec with Matchers with BeforeAndAfter { behavior of "#put" it should "call doPut on the concrete cache" in { - cache.put("foo")("bar", Some(1 second)) + cache.put("foo")("bar", Some(1 second)).unsafeRunSync() cache.putCalledWithArgs(0) should be(("foo", "bar", Some(1 second))) } it should "not call doPut on the concrete cache if cache writes are disabled" in { implicit val flags: Flags = Flags(writesEnabled = false) - cache.put("foo")("bar", Some(1 second)) + cache.put("foo")("bar", Some(1 second)).unsafeRunSync() cache.putCalledWithArgs should be(empty) } it should "call doPut with no TTL if the provided TTL is not finite" in { - cache.put("foo")("bar", Some(Duration.Inf)) + cache.put("foo")("bar", Some(Duration.Inf)).unsafeRunSync() cache.putCalledWithArgs(0) should be(("foo", "bar", None)) } behavior of "#remove" it should "call doRemove on the concrete cache" in { - cache.remove("baz") + cache.remove("baz").unsafeRunSync() cache.removeCalledWithArgs(0) should be("baz") } it should "concatenate key parts correctly" in { - cache.remove("hey", "yeah") + cache.remove("hey", "yeah").unsafeRunSync() cache.removeCalledWithArgs(0) should be("hey:yeah") } - behavior of "#caching (Scala Try mode)" + behavior of "#caching" it should "run the block and cache its result with no TTL if the value is not found in the cache" in { var called = false - val result = cache.caching("myKey")(None) { - called = true - "result of block" - } + val result = cache + .caching("myKey")(None) { + called = true + "result of block" + } + .unsafeRunSync() cache.getCalledWithArgs(0) should be("myKey") cache.putCalledWithArgs(0) should be("myKey", "result of block", None) @@ -93,10 +96,12 @@ class AbstractCacheSpec extends FlatSpec with Matchers with BeforeAndAfter { it should "run the block and cache its result with a TTL if the value is not found in the cache" in { var called = false - val result = cache.caching("myKey")(Some(5 seconds)) { - called = true - "result of block" - } + val result = cache + .caching("myKey")(Some(5 seconds)) { + called = true + "result of block" + } + .unsafeRunSync() cache.getCalledWithArgs(0) should be("myKey") cache.putCalledWithArgs(0) should be("myKey", "result of block", Some(5 seconds)) @@ -108,10 +113,12 @@ class AbstractCacheSpec extends FlatSpec with Matchers with BeforeAndAfter { cache.mmap.put("myKey", "value from cache") var called = false - val result = cache.caching("myKey")(None) { - called = true - "result of block" - } + val result = cache + .caching("myKey")(None) { + called = true + "result of block" + } + .unsafeRunSync() cache.getCalledWithArgs(0) should be("myKey") called should be(false) @@ -121,48 +128,52 @@ class AbstractCacheSpec extends FlatSpec with Matchers with BeforeAndAfter { behavior of "#cachingF (Scala Try mode)" it should "run the block and cache its result with no TTL if the value is not found in the cache" in { - import scalacache.modes.try_.mode var called = false - val tResult = cache.cachingF("myKey")(None) { - Try { - called = true - "result of block" + val tResult = cache + .cachingF("myKey")(None) { + SyncIO { + called = true + "result of block" + } } - } + .unsafeRunSync() cache.getCalledWithArgs(0) should be("myKey") cache.putCalledWithArgs(0) should be("myKey", "result of block", None) called should be(true) - tResult should be(Success("result of block")) + tResult should be("result of block") } it should "not run the block if the value is found in the cache" in { - import scalacache.modes.try_.mode cache.mmap.put("myKey", "value from cache") var called = false - val tResult = cache.cachingF("myKey")(None) { - Try { - called = true - "result of block" + val tResult = cache + .cachingF("myKey")(None) { + SyncIO { + called = true + "result of block" + } } - } + .unsafeRunSync() cache.getCalledWithArgs(0) should be("myKey") called should be(false) - tResult should be(Success("value from cache")) + tResult should be("value from cache") } behavior of "#caching (sync mode)" it should "run the block and cache its result if the value is not found in the cache" in { var called = false - val result = cache.caching("myKey")(None) { - called = true - "result of block" - } + val result = cache + .caching("myKey")(None) { + called = true + "result of block" + } + .unsafeRunSync() cache.getCalledWithArgs(0) should be("myKey") cache.putCalledWithArgs(0) should be("myKey", "result of block", None) @@ -174,10 +185,12 @@ class AbstractCacheSpec extends FlatSpec with Matchers with BeforeAndAfter { cache.mmap.put("myKey", "value from cache") var called = false - val result = cache.caching("myKey")(None) { - called = true - "result of block" - } + val result = cache + .caching("myKey")(None) { + called = true + "result of block" + } + .unsafeRunSync() cache.getCalledWithArgs(0) should be("myKey") called should be(false) @@ -192,10 +205,12 @@ class AbstractCacheSpec extends FlatSpec with Matchers with BeforeAndAfter { implicit val flags: Flags = Flags(readsEnabled = false) var called = false - val result = cache.caching("myKey")(None) { - called = true - "result of block" - } + val result = cache + .caching("myKey")(None) { + called = true + "result of block" + } + .unsafeRunSync() cache.getCalledWithArgs should be(empty) called should be(true) @@ -208,10 +223,12 @@ class AbstractCacheSpec extends FlatSpec with Matchers with BeforeAndAfter { implicit val flags: Flags = Flags(writesEnabled = false) var called = false - val result = cache.caching("myKey")(None) { - called = true - "result of block" - } + val result = cache + .caching("myKey")(None) { + called = true + "result of block" + } + .unsafeRunSync() cache.getCalledWithArgs(0) should be("myKey") called should be(true) @@ -227,10 +244,12 @@ class AbstractCacheSpec extends FlatSpec with Matchers with BeforeAndAfter { implicit val flags: Flags = Flags(readsEnabled = false) var called = false - val result = cache.cachingF[Id]("myKey")(None) { - called = true - "result of block" - } + val result = cache + .cachingF("myKey")(None) { + SyncIO { called = true } *> + SyncIO("result of block") + } + .unsafeRunSync() cache.getCalledWithArgs should be(empty) called should be(true) @@ -243,10 +262,12 @@ class AbstractCacheSpec extends FlatSpec with Matchers with BeforeAndAfter { implicit val flags: Flags = Flags(writesEnabled = false) var called = false - val result = cache.cachingF[Id]("myKey")(None) { - called = true - "result of block" - } + val result = cache + .cachingF("myKey")(None) { + SyncIO { called = true } *> + SyncIO("result of block") + } + .unsafeRunSync() cache.getCalledWithArgs(0) should be("myKey") called should be(true) diff --git a/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeyExcludingConstructorParamsSpec.scala b/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeyExcludingConstructorParamsSpec.scala index ad5894d8..859a3d84 100644 --- a/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeyExcludingConstructorParamsSpec.scala +++ b/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeyExcludingConstructorParamsSpec.scala @@ -4,6 +4,7 @@ import org.scalatest._ import scalacache._ import scalacache.memoization.MethodCallToStringConverter.excludeClassConstructorParams +import cats.effect.SyncIO class CacheKeyExcludingConstructorParamsSpec extends FlatSpec with CacheKeySpecCommon { self => @@ -13,18 +14,18 @@ class CacheKeyExcludingConstructorParamsSpec extends FlatSpec with CacheKeySpecC CacheConfig(memoization = MemoizationConfig(toStringConverter = excludeClassConstructorParams)) it should "not include the enclosing class's constructor params in the cache key" in { - val instance1 = new ClassWithConstructorParams(50) + val instance1 = new ClassWithConstructorParams[SyncIO](50) instance1.cache = cache - val instance2 = new ClassWithConstructorParams(100) + val instance2 = new ClassWithConstructorParams[SyncIO](100) instance2.cache = cache checkCacheKey("scalacache.memoization.ClassWithConstructorParams.foo(42)") { - instance1.foo(42) + instance1.foo(42).unsafeRunSync() } checkCacheKey("scalacache.memoization.ClassWithConstructorParams.foo(42)") { - instance2.foo(42) + instance2.foo(42).unsafeRunSync() } } @@ -36,57 +37,57 @@ class CacheKeyExcludingConstructorParamsSpec extends FlatSpec with CacheKeySpecC it should "call toString on arguments to convert them into a string" in { checkCacheKey("scalacache.memoization.CacheKeySpecCommon.takesCaseClass(custom toString)") { - takesCaseClass(CaseClass(1)) + takesCaseClass(CaseClass(1)).unsafeRunSync() } } it should "include values of lazy arguments" in { checkCacheKey("scalacache.memoization.CacheKeySpecCommon.lazyArg(1)") { - lazyArg(1) + lazyArg(1).unsafeRunSync() } } it should "exclude values of arguments annotated with @cacheKeyExclude" in { checkCacheKey("scalacache.memoization.CacheKeySpecCommon.withExcludedParams(1, 3)()") { - withExcludedParams(1, "2", "3")(4) + withExcludedParams(1, "2", "3")(4).unsafeRunSync() } } it should "work for a method inside a class" in { checkCacheKey("scalacache.memoization.AClass.insideClass(1)") { - new AClass().insideClass(1) + new AClass[SyncIO]().insideClass(1).unsafeRunSync() } } it should "work for a method inside a trait" in { checkCacheKey("scalacache.memoization.ATrait.insideTrait(1)") { - new ATrait { val cache = self.cache }.insideTrait(1) + new ATrait[SyncIO] { val cache = self.cache }.insideTrait(1).unsafeRunSync() } } it should "work for a method inside an object" in { AnObject.cache = this.cache checkCacheKey("scalacache.memoization.AnObject.insideObject(1)") { - AnObject.insideObject(1) + AnObject.insideObject(1).unsafeRunSync() } } it should "work for a method inside a class inside a class" in { checkCacheKey("scalacache.memoization.AClass.InnerClass.insideInnerClass(1)") { - new AClass().inner.insideInnerClass(1) + new AClass[SyncIO]().inner.insideInnerClass(1).unsafeRunSync() } } it should "work for a method inside an object inside a class" in { checkCacheKey("scalacache.memoization.AClass.InnerObject.insideInnerObject(1)") { - new AClass().InnerObject.insideInnerObject(1) + new AClass[SyncIO]().InnerObject.insideInnerObject(1).unsafeRunSync() } } it should "work for a method inside a package object" in { pkg.cache = this.cache checkCacheKey("scalacache.memoization.pkg.package.insidePackageObject(1)") { - pkg.insidePackageObject(1) + pkg.insidePackageObject(1).unsafeRunSync() } } diff --git a/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeyIncludingConstructorParamsSpec.scala b/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeyIncludingConstructorParamsSpec.scala index 34f910dd..cd70c977 100644 --- a/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeyIncludingConstructorParamsSpec.scala +++ b/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeyIncludingConstructorParamsSpec.scala @@ -3,6 +3,7 @@ package scalacache.memoization import org.scalatest._ import scalacache._ import scalacache.memoization.MethodCallToStringConverter._ +import cats.effect.SyncIO class CacheKeyIncludingConstructorParamsSpec extends FlatSpec with CacheKeySpecCommon { self => @@ -12,20 +13,20 @@ class CacheKeyIncludingConstructorParamsSpec extends FlatSpec with CacheKeySpecC CacheConfig(memoization = MemoizationConfig(toStringConverter = includeClassConstructorParams)) it should "include the enclosing class's constructor params in the cache key" in { - val instance = new ClassWithConstructorParams(50) + val instance = new ClassWithConstructorParams[SyncIO](50) instance.cache = cache checkCacheKey("scalacache.memoization.ClassWithConstructorParams(50).foo(42)") { - instance.foo(42) + instance.foo(42).unsafeRunSync() } } it should "exclude values of constructor params annotated with @cacheKeyExclude" in { - val instance = new ClassWithExcludedConstructorParam(50, 10) + val instance = new ClassWithExcludedConstructorParam[SyncIO](50, 10) instance.cache = cache checkCacheKey("scalacache.memoization.ClassWithExcludedConstructorParam(50).foo(42)") { - instance.foo(42) + instance.foo(42).unsafeRunSync() } } @@ -37,58 +38,58 @@ class CacheKeyIncludingConstructorParamsSpec extends FlatSpec with CacheKeySpecC it should "call toString on arguments to convert them into a string" in { checkCacheKey("scalacache.memoization.CacheKeySpecCommon.takesCaseClass(custom toString)") { - takesCaseClass(CaseClass(1)) + takesCaseClass(CaseClass(1)).unsafeRunSync() } } it should "include values of lazy arguments" in { checkCacheKey("scalacache.memoization.CacheKeySpecCommon.lazyArg(1)") { - lazyArg(1) + lazyArg(1).unsafeRunSync() } } it should "exclude values of arguments annotated with @cacheKeyExclude" in { checkCacheKey("scalacache.memoization.CacheKeySpecCommon.withExcludedParams(1, 3)()") { - withExcludedParams(1, "2", "3")(4) + withExcludedParams(1, "2", "3")(4).unsafeRunSync() } } it should "work for a method inside a class" in { // The class's implicit param (the Cache) should be included in the cache key) checkCacheKey(s"scalacache.memoization.AClass()(${cache.toString}).insideClass(1)") { - new AClass().insideClass(1) + new AClass[SyncIO]().insideClass(1).unsafeRunSync() } } it should "work for a method inside a trait" in { checkCacheKey("scalacache.memoization.ATrait.insideTrait(1)") { - new ATrait { val cache = self.cache }.insideTrait(1) + new ATrait[SyncIO] { val cache = self.cache }.insideTrait(1).unsafeRunSync() } } it should "work for a method inside an object" in { AnObject.cache = this.cache checkCacheKey("scalacache.memoization.AnObject.insideObject(1)") { - AnObject.insideObject(1) + AnObject.insideObject(1).unsafeRunSync() } } it should "work for a method inside a class inside a class" in { checkCacheKey("scalacache.memoization.AClass.InnerClass.insideInnerClass(1)") { - new AClass().inner.insideInnerClass(1) + new AClass[SyncIO]().inner.insideInnerClass(1).unsafeRunSync() } } it should "work for a method inside an object inside a class" in { checkCacheKey("scalacache.memoization.AClass.InnerObject.insideInnerObject(1)") { - new AClass().InnerObject.insideInnerObject(1) + new AClass[SyncIO]().InnerObject.insideInnerObject(1).unsafeRunSync() } } it should "work for a method inside a package object" in { pkg.cache = this.cache checkCacheKey("scalacache.memoization.pkg.package.insidePackageObject(1)") { - pkg.insidePackageObject(1) + pkg.insidePackageObject(1).unsafeRunSync() } } } diff --git a/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeySpecCommon.scala b/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeySpecCommon.scala index a2039b5c..5f49cc1a 100644 --- a/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeySpecCommon.scala +++ b/modules/core/shared/src/test/scala/scalacache/memoization/CacheKeySpecCommon.scala @@ -3,13 +3,13 @@ package scalacache.memoization import org.scalatest._ import scalacache._ -import scalacache.modes.sync._ +import cats.effect.SyncIO trait CacheKeySpecCommon extends Suite with Matchers with BeforeAndAfter { implicit def config: CacheConfig - implicit lazy val cache: MockCache[Int] = new MockCache[Int]()(config) + implicit lazy val cache: MockCache[SyncIO, Int] = new MockCache() before { cache.mmap.clear() @@ -20,77 +20,79 @@ trait CacheKeySpecCommon extends Suite with Matchers with BeforeAndAfter { val value = call // Check that the value is in the cache, with the expected key - cache.get(expectedKey) should be(Some(value)) + cache.get(expectedKey).unsafeRunSync() should be(Some(value)) } - def multipleArgLists(a: Int, b: String)(c: String, d: Int): Int = memoizeSync(None) { - 123 - } + def multipleArgLists(a: Int, b: String)(c: String, d: Int): Int = + memoize(None) { + 123 + }.unsafeRunSync() case class CaseClass(a: Int) { override def toString = "custom toString" } - def takesCaseClass(cc: CaseClass): Int = memoizeSync(None) { + + def takesCaseClass(cc: CaseClass): SyncIO[Int] = memoize(None) { 123 } - def lazyArg(a: => Int): Int = memoizeSync(None) { + def lazyArg(a: => Int): SyncIO[Int] = memoize(None) { 123 } - def functionArg(a: String => Int): Int = memoizeSync(None) { + def functionArg(a: String => Int): SyncIO[Int] = memoize(None) { 123 } - def withExcludedParams(a: Int, @cacheKeyExclude b: String, c: String)(@cacheKeyExclude d: Int): Int = - memoizeSync(None) { + def withExcludedParams(a: Int, @cacheKeyExclude b: String, c: String)(@cacheKeyExclude d: Int): SyncIO[Int] = + memoize(None) { 123 } } -class AClass(implicit cache: Cache[Int]) { - def insideClass(a: Int): Int = memoizeSync(None) { +class AClass[F[_]](implicit cache: Cache[F, Int]) { + def insideClass(a: Int): F[Int] = memoize(None) { 123 } class InnerClass { - def insideInnerClass(a: Int): Int = memoizeSync(None) { + def insideInnerClass(a: Int): F[Int] = memoize(None) { 123 } } val inner = new InnerClass object InnerObject { - def insideInnerObject(a: Int): Int = memoizeSync(None) { + def insideInnerObject(a: Int): F[Int] = memoize(None) { 123 } } } -trait ATrait { - implicit val cache: Cache[Int] +trait ATrait[F[_]] { + implicit val cache: Cache[F, Int] - def insideTrait(a: Int): Int = memoizeSync(None) { + def insideTrait(a: Int): F[Int] = memoize(None) { 123 } } object AnObject { - implicit var cache: Cache[Int] = null - def insideObject(a: Int): Int = memoizeSync(None) { + implicit var cache: Cache[SyncIO, Int] = null + def insideObject(a: Int): SyncIO[Int] = memoize(None) { 123 } } -class ClassWithConstructorParams(b: Int) { - implicit var cache: Cache[Int] = null - def foo(a: Int): Int = memoizeSync(None) { +class ClassWithConstructorParams[F[_]](b: Int) { + implicit var cache: Cache[F, Int] = null + def foo(a: Int): F[Int] = memoize(None) { a + b } } -class ClassWithExcludedConstructorParam(b: Int, @cacheKeyExclude c: Int) { - implicit var cache: Cache[Int] = null - def foo(a: Int): Int = memoizeSync(None) { +class ClassWithExcludedConstructorParam[F[_]](b: Int, @cacheKeyExclude c: Int) { + implicit var cache: Cache[F, Int] = null + def foo(a: Int): F[Int] = memoize(None) { a + b + c } } diff --git a/modules/core/shared/src/test/scala/scalacache/memoization/MemoizeSpec.scala b/modules/core/shared/src/test/scala/scalacache/memoization/MemoizeSpec.scala index e5716191..fde3684b 100644 --- a/modules/core/shared/src/test/scala/scalacache/memoization/MemoizeSpec.scala +++ b/modules/core/shared/src/test/scala/scalacache/memoization/MemoizeSpec.scala @@ -6,9 +6,10 @@ import scala.collection.mutable.ArrayBuffer import scala.concurrent.duration._ import scala.language.postfixOps import scalacache._ -import scalacache.modes.sync._ import scala.util.Try +import cats.effect.SyncIO +import cats.effect.Sync class MemoizeSpec extends FlatSpec with Matchers { @@ -17,12 +18,12 @@ class MemoizeSpec extends FlatSpec with Matchers { val expectedKey = "scalacache.memoization.MemoizeSpec.MyMockClass.myLongRunningMethod(123, abc)" it should "execute the block and cache the result, if there is a cache miss" in { - implicit val emptyCache = new EmptyCache[String] with LoggingCache[String] + implicit val emptyCache = new EmptyCache[SyncIO, String] with LoggingCache[SyncIO, String] val mockDbCall = new MockDbCall("hello") // should return the block's result - val result = new MyMockClass(mockDbCall).myLongRunningMethod(123, "abc") + val result = new MyMockClass(mockDbCall).myLongRunningMethod(123, "abc").unsafeRunSync() result should be("hello") // should check the cache first @@ -36,12 +37,12 @@ class MemoizeSpec extends FlatSpec with Matchers { } it should "not execute the block if there is a cache hit" in { - implicit val fullCache = new FullCache[String]("cache hit") with LoggingCache[String] + implicit val fullCache = new FullCache[SyncIO, String]("cache hit") with LoggingCache[SyncIO, String] val mockDbCall = new MockDbCall("hello") // should return the cached result - val result = new MyMockClass(mockDbCall).myLongRunningMethod(123, "abc") + val result = new MyMockClass(mockDbCall).myLongRunningMethod(123, "abc").unsafeRunSync() result should be("cache hit") // should check the cache first @@ -55,13 +56,13 @@ class MemoizeSpec extends FlatSpec with Matchers { } it should "execute the block if cache reads are disabled" in { - implicit val fullCache = new FullCache[String]("cache hit") with LoggingCache[String] + implicit val fullCache = new FullCache[SyncIO, String]("cache hit") with LoggingCache[SyncIO, String] implicit val flags = Flags(readsEnabled = false) val mockDbCall = new MockDbCall("hello") // should return the block's result - val result = new MyMockClass(mockDbCall).myLongRunningMethod(123, "abc") + val result = new MyMockClass(mockDbCall).myLongRunningMethod(123, "abc").unsafeRunSync() result should be("hello") // should NOT check the cache, because reads are disabled @@ -75,13 +76,13 @@ class MemoizeSpec extends FlatSpec with Matchers { } it should "not cache the result if cache writes are disabled" in { - implicit val emptyCache = new EmptyCache[String] with LoggingCache[String] + implicit val emptyCache = new EmptyCache[SyncIO, String] with LoggingCache[SyncIO, String] implicit val flags = Flags(writesEnabled = false) val mockDbCall = new MockDbCall("hello") // should return the block's result - val result = new MyMockClass(mockDbCall).myLongRunningMethod(123, "abc") + val result = new MyMockClass(mockDbCall).myLongRunningMethod(123, "abc").unsafeRunSync() result should be("hello") // should check the cache first @@ -97,20 +98,20 @@ class MemoizeSpec extends FlatSpec with Matchers { it should "work with a method argument called 'key'" in { // Reproduces https://github.com/cb372/scalacache/issues/13 """ - implicit val emptyCache = new EmptyCache[Int] with LoggingCache[Int] - def foo(key: Int): Int = memoizeSync(None) { + implicit val emptyCache = new EmptyCache[SyncIO, Int] with LoggingCache[SyncIO, Int] + def foo(key: Int): SyncIO[Int] = memoize(None) { key + 1 } """ should compile } it should "catch exceptions thrown by the cache" in { - implicit val dodgyCache = new ErrorRaisingCache[String] with LoggingCache[String] + implicit val dodgyCache = new ErrorRaisingCache[SyncIO, String] with LoggingCache[SyncIO, String] val mockDbCall = new MockDbCall("hello") // should return the block's result - val result = new MyMockClass(mockDbCall).myLongRunningMethod(123, "abc") + val result = new MyMockClass(mockDbCall).myLongRunningMethod(123, "abc").unsafeRunSync() result should be("hello") // should check the cache first @@ -128,12 +129,12 @@ class MemoizeSpec extends FlatSpec with Matchers { it should "pass the TTL parameter to the cache" in { val expectedKey = "scalacache.memoization.MemoizeSpec.MyMockClass.withTTL(123, abc)" - implicit val emptyCache = new EmptyCache[String] with LoggingCache[String] + implicit val emptyCache = new EmptyCache[SyncIO, String] with LoggingCache[SyncIO, String] val mockDbCall = new MockDbCall("hello") // should return the block's result - val result = new MyMockClass(mockDbCall).withTTL(123, "abc") + val result = new MyMockClass(mockDbCall).withTTL(123, "abc").unsafeRunSync() result should be("hello") // should check the cache first @@ -151,13 +152,12 @@ class MemoizeSpec extends FlatSpec with Matchers { it should "execute the block and cache the result, if there is a cache miss" in { val expectedKey = "scalacache.memoization.MemoizeSpec.MyMockClassWithTry.myLongRunningMethod(123, abc)" - implicit val emptyCache = new EmptyCache[String] with LoggingCache[String] - implicit val mode = scalacache.modes.try_.mode + implicit val emptyCache = new EmptyCache[SyncIO, String] with LoggingCache[SyncIO, String] val mockDbCall = new MockDbCall("hello") // should return the block's result - val result = new MyMockClassWithTry(mockDbCall).myLongRunningMethod(123, "abc").get + val result = new MyMockClassWithTry(mockDbCall).myLongRunningMethod(123, "abc").unsafeRunSync() result should be("hello") @@ -174,13 +174,12 @@ class MemoizeSpec extends FlatSpec with Matchers { it should "not execute the block if there is a cache hit" in { val expectedKey = "scalacache.memoization.MemoizeSpec.MyMockClassWithTry.myLongRunningMethod(123, abc)" - implicit val fullCache = new FullCache[String]("cache hit") with LoggingCache[String] - implicit val mode = scalacache.modes.try_.mode + implicit val fullCache = new FullCache[SyncIO, String]("cache hit") with LoggingCache[SyncIO, String] val mockDbCall = new MockDbCall("hello") // should return the cached result - val result = new MyMockClassWithTry(mockDbCall).myLongRunningMethod(123, "abc").get + val result = new MyMockClassWithTry(mockDbCall).myLongRunningMethod(123, "abc").unsafeRunSync() result should be("cache hit") @@ -197,13 +196,12 @@ class MemoizeSpec extends FlatSpec with Matchers { it should "catch exceptions thrown by the cache" in { val expectedKey = "scalacache.memoization.MemoizeSpec.MyMockClassWithTry.myLongRunningMethod(123, abc)" - implicit val dodgyCache = new ErrorRaisingCache[String] with LoggingCache[String] - implicit val mode = scalacache.modes.try_.mode + implicit val dodgyCache = new ErrorRaisingCache[SyncIO, String] with LoggingCache[SyncIO, String] val mockDbCall = new MockDbCall("hello") // should return the block's result - val result = new MyMockClassWithTry(mockDbCall).myLongRunningMethod(123, "abc").get + val result = new MyMockClassWithTry(mockDbCall).myLongRunningMethod(123, "abc").unsafeRunSync() result should be("hello") @@ -222,13 +220,12 @@ class MemoizeSpec extends FlatSpec with Matchers { it should "pass the TTL parameter to the cache" in { val expectedKey = "scalacache.memoization.MemoizeSpec.MyMockClassWithTry.withTTL(123, abc)" - implicit val emptyCache = new EmptyCache[String] with LoggingCache[String] - implicit val mode = scalacache.modes.try_.mode + implicit val emptyCache = new EmptyCache[SyncIO, String] with LoggingCache[SyncIO, String] val mockDbCall = new MockDbCall("hello") // should return the block's result - val result = new MyMockClassWithTry(mockDbCall).withTTL(123, "abc").get + val result = new MyMockClassWithTry(mockDbCall).withTTL(123, "abc").unsafeRunSync() result should be("hello") @@ -250,26 +247,30 @@ class MemoizeSpec extends FlatSpec with Matchers { } } - class MyMockClass(dbCall: Int => String)(implicit val cache: Cache[String], mode: Mode[Id], flags: Flags) { + class MyMockClass[F[_]](dbCall: Int => String)(implicit val cache: Cache[F, String], flags: Flags) { - def myLongRunningMethod(a: Int, b: String): String = memoizeSync(None) { + def myLongRunningMethod(a: Int, b: String): F[String] = memoize(None) { dbCall(a) } - def withTTL(a: Int, b: String): String = memoizeSync(Some(10 seconds)) { + def withTTL(a: Int, b: String): F[String] = memoize(Some(10 seconds)) { dbCall(a) } } - class MyMockClassWithTry(dbCall: Int => String)(implicit cache: Cache[String], mode: Mode[Try], flags: Flags) { + class MyMockClassWithTry[F[_]](dbCall: Int => String)( + implicit cache: Cache[F, String], + F: Sync[F], + flags: Flags + ) { - def myLongRunningMethod(a: Int, b: String): Try[String] = memoizeF(None) { - Try { dbCall(a) } + def myLongRunningMethod(a: Int, b: String): F[String] = memoizeF(None) { + F.delay { dbCall(a) } } - def withTTL(a: Int, b: String): Try[String] = memoizeF(Some(10 seconds)) { - Try { dbCall(a) } + def withTTL(a: Int, b: String): F[String] = memoizeF(Some(10 seconds)) { + F.delay { dbCall(a) } } } diff --git a/modules/core/shared/src/test/scala/scalacache/memoization/pkg/package.scala b/modules/core/shared/src/test/scala/scalacache/memoization/pkg/package.scala index a253df2b..d0557ff1 100644 --- a/modules/core/shared/src/test/scala/scalacache/memoization/pkg/package.scala +++ b/modules/core/shared/src/test/scala/scalacache/memoization/pkg/package.scala @@ -1,12 +1,14 @@ package scalacache.memoization import scalacache._ -import scalacache.modes.sync._ package object pkg { - implicit var cache: Cache[Int] = null - def insidePackageObject(a: Int): Int = memoizeSync(None) { + import cats.effect.SyncIO + + implicit var cache: Cache[SyncIO, Int] = null + + def insidePackageObject(a: Int): SyncIO[Int] = memoize(None) { 123 } diff --git a/modules/core/shared/src/test/scala/scalacache/mocks.scala b/modules/core/shared/src/test/scala/scalacache/mocks.scala index 5dbc1fc5..beb74c04 100644 --- a/modules/core/shared/src/test/scala/scalacache/mocks.scala +++ b/modules/core/shared/src/test/scala/scalacache/mocks.scala @@ -4,64 +4,68 @@ import scalacache.logging.Logger import scala.collection.mutable.ArrayBuffer import scala.concurrent.duration.Duration import scala.language.higherKinds +import cats.Applicative +import cats.effect.Sync +import cats.MonadError +import cats.Defer -class EmptyCache[V](implicit val config: CacheConfig) extends AbstractCache[V] { +class EmptyCache[F[_], V](implicit val F: Sync[F], val config: CacheConfig) extends AbstractCache[F, V] { override protected def logger = Logger.getLogger("EmptyCache") - override protected def doGet[F[_]](key: String)(implicit mode: Mode[F]) = - mode.M.pure(None) + override protected def doGet(key: String) = + F.pure(None) - override protected def doPut[F[_]](key: String, value: V, ttl: Option[Duration])(implicit mode: Mode[F]) = - mode.M.pure(()) + override protected def doPut(key: String, value: V, ttl: Option[Duration]) = + F.unit - override protected def doRemove[F[_]](key: String)(implicit mode: Mode[F]) = - mode.M.pure(()) + override protected def doRemove(key: String) = + F.unit - override protected def doRemoveAll[F[_]]()(implicit mode: Mode[F]) = - mode.M.pure(()) + override protected val doRemoveAll = + F.unit - override def close[F[_]]()(implicit mode: Mode[F]) = mode.M.pure(()) + override val close = F.unit } -class FullCache[V](value: V)(implicit val config: CacheConfig) extends AbstractCache[V] { +class FullCache[F[_], V](value: V)(implicit val F: Sync[F], val config: CacheConfig) extends AbstractCache[F, V] { override protected def logger = Logger.getLogger("FullCache") - override protected def doGet[F[_]](key: String)(implicit mode: Mode[F]) = - mode.M.pure(Some(value)) + override protected def doGet(key: String) = + F.pure(Some(value)) - override protected def doPut[F[_]](key: String, value: V, ttl: Option[Duration])(implicit mode: Mode[F]) = - mode.M.pure(()) + override protected def doPut(key: String, value: V, ttl: Option[Duration]) = + F.unit - override protected def doRemove[F[_]](key: String)(implicit mode: Mode[F]) = - mode.M.pure(()) + override protected def doRemove(key: String) = + F.unit - override protected def doRemoveAll[F[_]]()(implicit mode: Mode[F]) = - mode.M.pure(()) + override protected val doRemoveAll = + F.unit - override def close[F[_]]()(implicit mode: Mode[F]) = mode.M.pure(()) + override val close = F.unit } -class ErrorRaisingCache[V](implicit val config: CacheConfig) extends AbstractCache[V] { +class ErrorRaisingCache[F[_], V](implicit val F: Sync[F], val config: CacheConfig) extends AbstractCache[F, V] { - override protected def logger = Logger.getLogger("FullCache") + override protected val logger = Logger.getLogger("FullCache") - override protected def doGet[F[_]](key: String)(implicit mode: Mode[F]) = - mode.M.raiseError(new RuntimeException("failed to read")) + override protected def doGet(key: String) = + F.raiseError(new RuntimeException("failed to read")) - override protected def doPut[F[_]](key: String, value: V, ttl: Option[Duration])(implicit mode: Mode[F]) = - mode.M.raiseError(new RuntimeException("failed to write")) + override protected def doPut(key: String, value: V, ttl: Option[Duration]) = + F.raiseError(new RuntimeException("failed to write")) - override protected def doRemove[F[_]](key: String)(implicit mode: Mode[F]) = - mode.M.pure(()) + override protected def doRemove(key: String) = + F.unit - override protected def doRemoveAll[F[_]]()(implicit mode: Mode[F]) = - mode.M.pure(()) + override protected val doRemoveAll = + F.unit - override def close[F[_]]()(implicit mode: Mode[F]) = mode.M.pure(()) + override val close = F.unit } @@ -69,25 +73,25 @@ class ErrorRaisingCache[V](implicit val config: CacheConfig) extends AbstractCac * A mock cache for use in tests and samples. * Does not support TTL. */ -class MockCache[V](implicit val config: CacheConfig) extends AbstractCache[V] { +class MockCache[F[_], V](implicit val F: Sync[F], val config: CacheConfig) extends AbstractCache[F, V] { override protected def logger = Logger.getLogger("MockCache") val mmap = collection.mutable.Map[String, V]() - override protected def doGet[F[_]](key: String)(implicit mode: Mode[F]) = - mode.M.delay(mmap.get(key)) + override protected def doGet(key: String) = + F.delay(mmap.get(key)) - override protected def doPut[F[_]](key: String, value: V, ttl: Option[Duration])(implicit mode: Mode[F]) = - mode.M.delay(mmap.put(key, value)) + override protected def doPut(key: String, value: V, ttl: Option[Duration]) = + F.delay(mmap.put(key, value)) - override protected def doRemove[F[_]](key: String)(implicit mode: Mode[F]) = - mode.M.delay(mmap.remove(key)) + override protected def doRemove(key: String) = + F.delay(mmap.remove(key)) - override protected def doRemoveAll[F[_]]()(implicit mode: Mode[F]) = - mode.M.delay(mmap.clear()) + override protected val doRemoveAll = + F.delay(mmap.clear()) - override def close[F[_]]()(implicit mode: Mode[F]) = mode.M.pure(()) + override val close = F.unit } @@ -95,28 +99,28 @@ class MockCache[V](implicit val config: CacheConfig) extends AbstractCache[V] { * A cache that keeps track of the arguments it was called with. Useful for tests. * Designed to be mixed in as a stackable trait. */ -trait LoggingCache[V] extends AbstractCache[V] { +trait LoggingCache[F[_], V] extends AbstractCache[F, V] { + val F: Sync[F] + var (getCalledWithArgs, putCalledWithArgs, removeCalledWithArgs) = (ArrayBuffer.empty[String], ArrayBuffer.empty[(String, Any, Option[Duration])], ArrayBuffer.empty[String]) - protected abstract override def doGet[F[_]](key: String)(implicit mode: Mode[F]): F[Option[V]] = { + protected abstract override def doGet(key: String): F[Option[V]] = F.suspend { getCalledWithArgs.append(key) super.doGet(key) } - protected abstract override def doPut[F[_]](key: String, value: V, ttl: Option[Duration])( - implicit mode: Mode[F] - ): F[Any] = { + protected abstract override def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] = F.suspend { putCalledWithArgs.append((key, value, ttl)) super.doPut(key, value, ttl) } - protected abstract override def doRemove[F[_]](key: String)(implicit mode: Mode[F]): F[Any] = { + protected abstract override def doRemove(key: String): F[Unit] = F.suspend { removeCalledWithArgs.append(key) super.doRemove(key) } - def reset(): Unit = { + val reset: F[Unit] = F.delay { getCalledWithArgs.clear() putCalledWithArgs.clear() removeCalledWithArgs.clear() @@ -127,4 +131,4 @@ trait LoggingCache[V] extends AbstractCache[V] { /** * A mock cache that keeps track of the arguments it was called with. */ -class LoggingMockCache[V] extends MockCache[V] with LoggingCache[V] +class LoggingMockCache[F[_]: Sync, V] extends MockCache[F, V] with LoggingCache[F, V] From 1894d657f749f1273a4d9fc7630d5a95764db0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 22 Mar 2020 19:53:03 +0100 Subject: [PATCH 08/21] F.unit --- .../core/shared/src/main/scala/scalacache/AbstractCache.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala b/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala index 906c0434..c1c9b1f2 100644 --- a/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala +++ b/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala @@ -54,7 +54,7 @@ trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { if (logger.isDebugEnabled) { logger.debug(s"Skipping cache PUT because cache writes are disabled. Key: $key") } - F.pure(()) + F.unit } } From 4259a02161c965512ec2626abca8f83e3bb44543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 22 Mar 2020 19:59:56 +0100 Subject: [PATCH 09/21] Remove Scalaz module (there are CE instances for Task) --- build.sbt | 13 +----- .../src/main/scala/scalacache/Scalaz72.scala | 40 ------------------- 2 files changed, 1 insertion(+), 52 deletions(-) delete mode 100644 modules/scalaz72/src/main/scala/scalacache/Scalaz72.scala diff --git a/build.sbt b/build.sbt index 263eacd3..b4050a62 100644 --- a/build.sbt +++ b/build.sbt @@ -30,7 +30,6 @@ lazy val root: Project = Project(id = "scalacache", base = file(".")) redis, caffeine, catsEffect, - scalaz72, circe, tests ) @@ -106,15 +105,6 @@ lazy val catsEffect = jvmOnlyModule("cats-effect") coverageFailOnMinimum := true ) -lazy val scalaz72 = jvmOnlyModule("scalaz72") - .settings( - libraryDependencies ++= Seq( - "org.scalaz" %% "scalaz-concurrent" % "7.2.30" - ), - coverageMinimum := 40, - coverageFailOnMinimum := true - ) - def circeVersion(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { case Some((2, scalaMajor)) if scalaMajor >= 12 => "0.13.0" @@ -137,7 +127,7 @@ lazy val circe = jvmOnlyModule("circe") lazy val tests = jvmOnlyModule("tests") .settings(publishArtifact := false) - .dependsOn(caffeine, memcached, redis, catsEffect, scalaz72, circe) + .dependsOn(caffeine, memcached, redis, catsEffect, circe) lazy val docs = jvmOnlyModule("docs") .enablePlugins(MicrositesPlugin) @@ -162,7 +152,6 @@ lazy val docs = jvmOnlyModule("docs") redis, caffeine, catsEffect, - scalaz72, circe ) diff --git a/modules/scalaz72/src/main/scala/scalacache/Scalaz72.scala b/modules/scalaz72/src/main/scala/scalacache/Scalaz72.scala deleted file mode 100644 index 19ed39b4..00000000 --- a/modules/scalaz72/src/main/scala/scalacache/Scalaz72.scala +++ /dev/null @@ -1,40 +0,0 @@ -package scalacache - -import scala.util.control.NonFatal -import scalaz.\/ -import scalaz.concurrent.{Future, Task} - -object Scalaz72 { - - object modes { - - implicit val task: Mode[Task] = new Mode[Task] { - val M: Async[Task] = AsyncForScalazTask - } - - } - - val AsyncForScalazTask: Async[Task] = new Async[Task] { - - def pure[A](a: A): Task[A] = Task.now(a) - - def map[A, B](fa: Task[A])(f: (A) => B): Task[B] = fa.map(f) - - def flatMap[A, B](fa: Task[A])(f: (A) => Task[B]): Task[B] = fa.flatMap(f) - - def raiseError[A](t: Throwable): Task[A] = Task.fail(t) - - def handleNonFatal[A](fa: => Task[A])(f: Throwable => A): Task[A] = fa.handle { - case NonFatal(e) => f(e) - } - - def delay[A](thunk: => A): Task[A] = Task.delay(thunk) - - def suspend[A](thunk: => Task[A]): Task[A] = Task.suspend(thunk) - - def async[A](register: (Either[Throwable, A] => Unit) => Unit): Task[A] = - new Task(Future.async(register).map(\/.fromEither)) - - } - -} From 290e453245757969d1ee52eb965fbb6c79e5ca65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 31 May 2020 17:54:38 +0200 Subject: [PATCH 10/21] Bump cats-effect --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index b4050a62..57eae6b8 100644 --- a/build.sbt +++ b/build.sbt @@ -41,7 +41,7 @@ lazy val core = moduleName := "scalacache-core", libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value, - "org.typelevel" %% "cats-effect" % "2.1.2", + "org.typelevel" %% "cats-effect" % "2.1.3", "org.scalatest" %%% "scalatest" % "3.0.8" % Test, "org.scalacheck" %%% "scalacheck" % "1.14.3" % Test ), From dc92b55bc6ec4e06551d5018f14a1c398a1f60f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 31 May 2020 18:54:57 +0200 Subject: [PATCH 11/21] Hopefully compile all tests --- build.sbt | 2 +- .../benchmark/CaffeineBenchmark.scala | 4 +- .../benchmark/ProfilingMemoize.scala | 6 +- .../scalacache/caffeine/CaffeineCache.scala | 22 +++--- .../scala/scalacache/logging/Logger.scala | 21 +++--- .../main/scala/scalacache/AbstractCache.scala | 34 ++++----- .../scala/scalacache/LoggingSupport.scala | 25 ++++--- .../scalacache/memcached/MemcachedCache.scala | 42 ++++++----- .../scala/scalacache/redis/RedisCache.scala | 25 +++---- .../scalacache/redis/RedisCacheBase.scala | 73 +++++++++---------- .../scalacache/redis/RedisClusterCache.scala | 42 ++++++----- .../scalacache/redis/SentinelRedisCache.scala | 42 ++++++----- .../scalacache/redis/ShardedRedisCache.scala | 28 ++++--- .../integrationtests/IntegrationTests.scala | 47 +++--------- 14 files changed, 202 insertions(+), 211 deletions(-) diff --git a/build.sbt b/build.sbt index 57eae6b8..43dc6b10 100644 --- a/build.sbt +++ b/build.sbt @@ -182,7 +182,7 @@ lazy val commonSettings = mavenSettings ++ Seq( organization := "com.github.cb372", - scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature"), + scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature", "-language:higherKinds"), parallelExecution in Test := false ) diff --git a/modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala b/modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala index 7c1ad7ae..8fd77cc6 100644 --- a/modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala +++ b/modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala @@ -14,10 +14,10 @@ import scalacache.modes.sync._ @State(Scope.Thread) class CaffeineBenchmark { - val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]() + val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]() implicit val cache: Cache[String] = CaffeineCache(underlyingCache) - val key = "key" + val key = "key" val value: String = "value" def itemCachedNoMemoize(key: String): Id[Option[String]] = { diff --git a/modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala b/modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala index 3836ee62..d3836375 100644 --- a/modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala +++ b/modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala @@ -13,9 +13,9 @@ import scalacache.modes.sync._ object ProfilingMemoize extends App { val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]() - implicit val cache = CaffeineCache[String](underlyingCache) + implicit val cache = CaffeineCache[String](underlyingCache) - val key = "key" + val key = "key" val value: String = "value" def itemCachedMemoize(key: String): String = memoizeSync(None) { @@ -23,7 +23,7 @@ object ProfilingMemoize extends App { } var result: String = _ - var i = 0L + var i = 0L while (i < Long.MaxValue) { result = itemCachedMemoize(key) diff --git a/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala b/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala index a821c66a..747d7126 100644 --- a/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala +++ b/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala @@ -13,19 +13,17 @@ import cats.effect.Sync import java.util.concurrent.TimeUnit import cats.implicits._ import cats.MonadError -import cats.Defer /* * Thin wrapper around Caffeine. * * This cache implementation is synchronous. */ -class CaffeineCache[F[_], V](val underlying: CCache[String, Entry[F, V]])( +class CaffeineCache[F[_]: Sync, V](val underlying: CCache[String, Entry[F, V]])( implicit val config: CacheConfig, - clock: Clock[F], - val F: Sync[F] + clock: Clock[F] ) extends AbstractCache[F, V] { - protected implicit val Defer: Defer[F] = F + protected val F: Sync[F] = Sync[F] override protected final val logger = Logger.getLogger(getClass.getName) @@ -36,13 +34,11 @@ class CaffeineCache[F[_], V](val underlying: CCache[String, Entry[F, V]])( .flatMap(_.filterA(_.isExpired)) .map(_.map(_.value)) .flatTap { result => - F.delay { - logCacheHitOrMiss(key, result) - } + logCacheHitOrMiss(key, result) } } - def doPut(key: String, value: V, ttl: Option[Duration]): F[Any] = ttl.traverse(toExpiryTime).flatMap { expiry => + def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] = ttl.traverse(toExpiryTime).flatMap { expiry => F.delay { val entry = Entry[F, V](value, expiry) underlying.put(key, entry) @@ -50,15 +46,15 @@ class CaffeineCache[F[_], V](val underlying: CCache[String, Entry[F, V]])( } } - override def doRemove(key: String): F[Any] = + override def doRemove(key: String): F[Unit] = F.delay(underlying.invalidate(key)) - override def doRemoveAll(): F[Any] = + override def doRemoveAll(): F[Unit] = F.delay(underlying.invalidateAll()) - override def close: F[Any] = { + override def close: F[Unit] = { // Nothing to do - F.pure(()) + F.unit } private def toExpiryTime(ttl: Duration): F[Instant] = diff --git a/modules/core/jvm/src/main/scala/scalacache/logging/Logger.scala b/modules/core/jvm/src/main/scala/scalacache/logging/Logger.scala index 00d3bbc3..0b9f379c 100644 --- a/modules/core/jvm/src/main/scala/scalacache/logging/Logger.scala +++ b/modules/core/jvm/src/main/scala/scalacache/logging/Logger.scala @@ -1,23 +1,26 @@ package scalacache.logging import org.slf4j.{Logger => Slf4jLogger, LoggerFactory} +import cats.effect.Sync +import cats.Applicative +import cats.implicits._ object Logger { - - def getLogger(name: String): Logger = new Logger(LoggerFactory.getLogger(name)) - + def getLogger[F[_]: Sync](name: String): Logger[F] = new Logger[F](LoggerFactory.getLogger(name)) } -final class Logger(logger: Slf4jLogger) { +final class Logger[F[_]: Sync](private val logger: Slf4jLogger) { + private def whenM[A](fb: F[Boolean])(fa: => F[A]): F[Option[A]] = fb.ifM(fa.map(_.some), Applicative[F].pure(None)) - def isDebugEnabled: Boolean = logger.isDebugEnabled + def ifDebugEnabled[A](fa: => F[A]): F[Option[A]] = + whenM(Sync[F].delay(logger.isDebugEnabled))(fa) - def isWarnEnabled: Boolean = logger.isWarnEnabled + def ifWarnEnabled[A](fa: => F[A]): F[Option[A]] = whenM(Sync[F].delay(logger.isWarnEnabled))(fa) - def debug(message: String): Unit = logger.debug(message) + def debug(message: String): F[Unit] = Sync[F].delay(logger.debug(message)) - def warn(message: String): Unit = logger.warn(message) + def warn(message: String): F[Unit] = Sync[F].delay(logger.warn(message)) - def warn(message: String, e: Throwable): Unit = logger.warn(message, e) + def warn(message: String, e: Throwable): F[Unit] = Sync[F].delay(logger.warn(message, e)) } diff --git a/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala b/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala index c1c9b1f2..363a3a0b 100644 --- a/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala +++ b/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala @@ -7,6 +7,7 @@ import cats.Monad import cats.implicits._ import cats.MonadError import cats.effect.Sync +import cats.Applicative /** * An abstract implementation of [[CacheAlg]] that takes care of @@ -17,7 +18,7 @@ import cats.effect.Sync * * @tparam V The value of types stored in the cache. */ -trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { +trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport[F] { protected implicit def F: Sync[F] // GET @@ -27,12 +28,12 @@ trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { private def checkFlagsAndGet(key: String)(implicit flags: Flags): F[Option[V]] = { if (flags.readsEnabled) { doGet(key) - } else { - if (logger.isDebugEnabled) { - logger.debug(s"Skipping cache GET because cache reads are disabled. Key: $key") - } - F.pure(None) - } + } else + logger + .ifDebugEnabled { + logger.debug(s"Skipping cache GET because cache reads are disabled. Key: $key") + } + .as(None) } final override def get(keyParts: Any*)(implicit flags: Flags): F[Option[V]] = { @@ -50,12 +51,10 @@ trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { ): F[Unit] = { if (flags.writesEnabled) { doPut(key, value, ttl) - } else { - if (logger.isDebugEnabled) { + } else + logger.ifDebugEnabled { logger.debug(s"Skipping cache PUT because cache writes are disabled. Key: $key") - } - F.unit - } + }.void } final override def put( @@ -122,11 +121,10 @@ trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { flags: Flags ): F[V] = { checkFlagsAndGet(key) - .handleError { e => - if (logger.isWarnEnabled) { - logger.warn(s"Failed to read from cache. Key = $key", e) - } - None + .handleErrorWith { e => + logger + .ifWarnEnabled(logger.warn(s"Failed to read from cache. Key = $key", e)) + .as(None) } .flatMap { case Some(valueFromCache) => F.pure(valueFromCache) @@ -134,7 +132,7 @@ trait AbstractCache[F[_], V] extends Cache[F, V] with LoggingSupport { f.flatTap { calculatedValue => checkFlagsAndPut(key, calculatedValue, ttl) .handleError { e => - if (logger.isWarnEnabled) { + logger.ifWarnEnabled { logger.warn(s"Failed to write to cache. Key = $key", e) } } diff --git a/modules/core/shared/src/main/scala/scalacache/LoggingSupport.scala b/modules/core/shared/src/main/scala/scalacache/LoggingSupport.scala index 674c47cd..86acdcfd 100644 --- a/modules/core/shared/src/main/scala/scalacache/LoggingSupport.scala +++ b/modules/core/shared/src/main/scala/scalacache/LoggingSupport.scala @@ -3,13 +3,17 @@ package scalacache import scalacache.logging.Logger import scala.concurrent.duration.Duration +import cats.effect.Sync +import cats.Applicative +import cats.Monad +import cats.implicits._ /** * Helper methods for logging */ -trait LoggingSupport { - - protected def logger: Logger +trait LoggingSupport[F[_]] { + protected def logger: Logger[F] + protected implicit def F: Monad[F] /** * Output a debug log to record the result of a cache lookup @@ -18,12 +22,11 @@ trait LoggingSupport { * @param result the result of the cache lookup * @tparam A the type of the cache value */ - protected def logCacheHitOrMiss[A](key: String, result: Option[A]): Unit = { - if (logger.isDebugEnabled) { + protected def logCacheHitOrMiss[A](key: String, result: Option[A]): F[Unit] = + logger.ifDebugEnabled { val hitOrMiss = result.map(_ => "hit") getOrElse "miss" logger.debug(s"Cache $hitOrMiss for key $key") - } - } + }.void /** * Output a debug log to record a cache insertion/update @@ -31,11 +34,9 @@ trait LoggingSupport { * @param key the key that was inserted/updated * @param ttl the TTL of the inserted entry */ - protected def logCachePut(key: String, ttl: Option[Duration]): Unit = { - if (logger.isDebugEnabled) { + protected def logCachePut(key: String, ttl: Option[Duration]): F[Unit] = + logger.ifDebugEnabled { val ttlMsg = ttl.map(d => s" with TTL ${d.toMillis} ms") getOrElse "" logger.debug(s"Inserted value into cache with key $key$ttlMsg") - } - } - + }.void } diff --git a/modules/memcached/src/main/scala/scalacache/memcached/MemcachedCache.scala b/modules/memcached/src/main/scala/scalacache/memcached/MemcachedCache.scala index f36c8fe2..33498a28 100644 --- a/modules/memcached/src/main/scala/scalacache/memcached/MemcachedCache.scala +++ b/modules/memcached/src/main/scala/scalacache/memcached/MemcachedCache.scala @@ -6,29 +6,33 @@ import net.spy.memcached.{AddrUtil, BinaryConnectionFactory, MemcachedClient} import scalacache.logging.Logger import scalacache.serialization.Codec -import scalacache.{AbstractCache, CacheConfig, Mode} +import scalacache.{AbstractCache, CacheConfig} import scala.concurrent.duration.Duration import scala.util.Success import scala.language.higherKinds import scala.util.control.NonFatal +import cats.effect.Async +import cats.effect.Sync class MemcachedException(message: String) extends Exception(message) /** * Wrapper around spymemcached */ -class MemcachedCache[V]( +class MemcachedCache[F[_]: Async, V]( val client: MemcachedClient, val keySanitizer: MemcachedKeySanitizer = ReplaceAndTruncateSanitizer() )(implicit val config: CacheConfig, codec: Codec[V]) - extends AbstractCache[V] + extends AbstractCache[F, V] with MemcachedTTLConverter { + protected def F: Async[F] = Async[F] + override protected final val logger = - Logger.getLogger(getClass.getName) + Logger.getLogger[F](getClass.getName) - override protected def doGet[F[_]](key: String)(implicit mode: Mode[F]): F[Option[V]] = { - mode.M.async { cb => + override protected def doGet(key: String): F[Option[V]] = { + F.async { cb => val f = client.asyncGet(keySanitizer.toValidMemcachedKey(key)) f.addListener(new GetCompletionListener { def onComplete(g: GetFuture[_]): Unit = { @@ -52,8 +56,8 @@ class MemcachedCache[V]( } } - override protected def doPut[F[_]](key: String, value: V, ttl: Option[Duration])(implicit mode: Mode[F]): F[Any] = { - mode.M.async { cb => + override protected def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] = { + F.async { cb => val valueToSend = codec.encode(value) val f = client.set(keySanitizer.toValidMemcachedKey(key), toMemcachedExpiry(ttl), valueToSend) f.addListener(new OperationCompletionListener { @@ -70,8 +74,8 @@ class MemcachedCache[V]( } } - override protected def doRemove[F[_]](key: String)(implicit mode: Mode[F]): F[Any] = { - mode.M.async { cb => + override protected def doRemove(key: String): F[Unit] = { + F.async { cb => val f = client.delete(key) f.addListener(new OperationCompletionListener { def onComplete(g: OperationFuture[_]): Unit = { @@ -84,8 +88,8 @@ class MemcachedCache[V]( } } - override protected def doRemoveAll[F[_]]()(implicit mode: Mode[F]): F[Any] = { - mode.M.async { cb => + override protected def doRemoveAll: F[Unit] = { + F.async { cb => val f = client.flush() f.addListener(new OperationCompletionListener { def onComplete(g: OperationFuture[_]): Unit = { @@ -98,7 +102,7 @@ class MemcachedCache[V]( } } - override def close[F[_]]()(implicit mode: Mode[F]): F[Any] = mode.M.delay(client.shutdown()) + override def close: F[Unit] = F.delay(client.shutdown()) } @@ -107,7 +111,7 @@ object MemcachedCache { /** * Create a Memcached client connecting to localhost:11211 and use it for caching */ - def apply[V](implicit config: CacheConfig, codec: Codec[V]): MemcachedCache[V] = + def apply[F[_]: Async, V](implicit config: CacheConfig, codec: Codec[V]): MemcachedCache[F, V] = apply("localhost:11211") /** @@ -115,7 +119,9 @@ object MemcachedCache { * * @param addressString Address string, with addresses separated by spaces, e.g. "host1:11211 host2:22322" */ - def apply[V](addressString: String)(implicit config: CacheConfig, codec: Codec[V]): MemcachedCache[V] = + def apply[F[_]: Async, V]( + addressString: String + )(implicit config: CacheConfig, codec: Codec[V]): MemcachedCache[F, V] = apply(new MemcachedClient(new BinaryConnectionFactory(), AddrUtil.getAddresses(addressString))) /** @@ -123,7 +129,9 @@ object MemcachedCache { * * @param client Memcached client */ - def apply[V](client: MemcachedClient)(implicit config: CacheConfig, codec: Codec[V]): MemcachedCache[V] = - new MemcachedCache[V](client) + def apply[F[_]: Async, V]( + client: MemcachedClient + )(implicit config: CacheConfig, codec: Codec[V]): MemcachedCache[F, V] = + new MemcachedCache[F, V](client) } diff --git a/modules/redis/src/main/scala/scalacache/redis/RedisCache.scala b/modules/redis/src/main/scala/scalacache/redis/RedisCache.scala index fc03062c..07c8f4cf 100644 --- a/modules/redis/src/main/scala/scalacache/redis/RedisCache.scala +++ b/modules/redis/src/main/scala/scalacache/redis/RedisCache.scala @@ -3,26 +3,23 @@ package scalacache.redis import redis.clients.jedis._ import scala.language.higherKinds -import scalacache.{CacheConfig, Mode} +import scalacache.{CacheConfig} import scalacache.serialization.Codec +import cats.effect.Resource +import cats.effect.Sync /** * Thin wrapper around Jedis */ -class RedisCache[V](val jedisPool: JedisPool)(implicit val config: CacheConfig, val codec: Codec[V]) - extends RedisCacheBase[V] { +class RedisCache[F[_]: Sync, V](val jedisPool: JedisPool)(implicit val config: CacheConfig, val codec: Codec[V]) + extends RedisCacheBase[F, V] { + protected def F: Sync[F] = Sync[F] type JClient = Jedis - protected def doRemoveAll[F[_]]()(implicit mode: Mode[F]): F[Any] = mode.M.delay { - val jedis = jedisPool.getResource() - try { - jedis.flushDB() - } finally { - jedis.close() - } + protected val doRemoveAll: F[Unit] = withJedis { jedis => + F.delay(jedis.flushDB()) } - } object RedisCache { @@ -30,14 +27,14 @@ object RedisCache { /** * Create a Redis client connecting to the given host and use it for caching */ - def apply[V](host: String, port: Int)(implicit config: CacheConfig, codec: Codec[V]): RedisCache[V] = + def apply[F[_]: Sync, V](host: String, port: Int)(implicit config: CacheConfig, codec: Codec[V]): RedisCache[F, V] = apply(new JedisPool(host, port)) /** * Create a cache that uses the given Jedis client pool * @param jedisPool a Jedis pool */ - def apply[V](jedisPool: JedisPool)(implicit config: CacheConfig, codec: Codec[V]): RedisCache[V] = - new RedisCache[V](jedisPool) + def apply[F[_]: Sync, V](jedisPool: JedisPool)(implicit config: CacheConfig, codec: Codec[V]): RedisCache[F, V] = + new RedisCache[F, V](jedisPool) } diff --git a/modules/redis/src/main/scala/scalacache/redis/RedisCacheBase.scala b/modules/redis/src/main/scala/scalacache/redis/RedisCacheBase.scala index 6f8f17bd..da448ba0 100644 --- a/modules/redis/src/main/scala/scalacache/redis/RedisCacheBase.scala +++ b/modules/redis/src/main/scala/scalacache/redis/RedisCacheBase.scala @@ -7,17 +7,19 @@ import redis.clients.util.Pool import scalacache.logging.Logger import scalacache.serialization.Codec -import scalacache.{AbstractCache, CacheConfig, Mode} +import scalacache.{AbstractCache, CacheConfig} import scala.concurrent.duration._ -import scala.language.higherKinds +import cats.effect.Resource +import cats.effect.Sync +import cats.implicits._ /** * Contains implementations of all methods that can be implemented independent of the type of Redis client. * This is everything apart from `removeAll`, which needs to be implemented differently for sharded Redis. */ -trait RedisCacheBase[V] extends AbstractCache[V] { +trait RedisCacheBase[F[_], V] extends AbstractCache[F, V] { - override protected final val logger = Logger.getLogger(getClass.getName) + override protected final val logger = Logger.getLogger[F](getClass.getName) import StringEnrichment.StringWithUtf8Bytes @@ -29,7 +31,7 @@ trait RedisCacheBase[V] extends AbstractCache[V] { protected def codec: Codec[V] - protected def doGet[F[_]](key: String)(implicit mode: Mode[F]): F[Option[V]] = mode.M.suspend { + protected def doGet(key: String): F[Option[V]] = withJedis { jedis => val bytes = jedis.get(key.utf8bytes) val result: Codec.DecodingResult[Option[V]] = { @@ -38,60 +40,53 @@ trait RedisCacheBase[V] extends AbstractCache[V] { else Right(None) } + result match { case Left(e) => - mode.M.raiseError(e) + F.raiseError[Option[V]](e) case Right(maybeValue) => - logCacheHitOrMiss(key, maybeValue) - mode.M.pure(maybeValue) + logCacheHitOrMiss(key, maybeValue).as(maybeValue) } } - } - protected def doPut[F[_]](key: String, value: V, ttl: Option[Duration])(implicit mode: Mode[F]): F[Any] = - mode.M.delay { - withJedis { jedis => - val keyBytes = key.utf8bytes - val valueBytes = codec.encode(value) - ttl match { - case None => jedis.set(keyBytes, valueBytes) - case Some(Duration.Zero) => jedis.set(keyBytes, valueBytes) - case Some(d) if d < 1.second => - if (logger.isWarnEnabled) { - logger.warn( - s"Because Redis (pre 2.6.12) does not support sub-second expiry, TTL of $d will be rounded up to 1 second" - ) - } + protected def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] = { + withJedis { jedis => + val keyBytes = key.utf8bytes + val valueBytes = codec.encode(value) + ttl match { + case None => F.delay(jedis.set(keyBytes, valueBytes)) + case Some(Duration.Zero) => F.delay(jedis.set(keyBytes, valueBytes)) + case Some(d) if d < 1.second => + logger.ifWarnEnabled { + logger.warn( + s"Because Redis (pre 2.6.12) does not support sub-second expiry, TTL of $d will be rounded up to 1 second" + ) + } *> F.delay { jedis.setex(keyBytes, 1, valueBytes) - case Some(d) => - jedis.setex(keyBytes, d.toSeconds.toInt, valueBytes) - } + } + case Some(d) => + F.delay(jedis.setex(keyBytes, d.toSeconds.toInt, valueBytes)) } - logCachePut(key, ttl) - } + } *> logCachePut(key, ttl) + } - protected def doRemove[F[_]](key: String)(implicit mode: Mode[F]): F[Any] = mode.M.delay { + protected def doRemove(key: String): F[Unit] = { withJedis { jedis => - jedis.del(key.utf8bytes) + F.delay(jedis.del(key.utf8bytes)) } } - def close[F[_]]()(implicit mode: Mode[F]): F[Any] = mode.M.delay(jedisPool.close()) + val close: F[Unit] = F.delay(jedisPool.close()) /** * Borrow a Jedis client from the pool, perform some operation and then return the client to the pool. * - * @param f block that uses the Jedis client + * @param f block that uses the Jedis client. * @tparam T return type of the block * @return the result of executing the block */ - protected final def withJedis[T](f: JClient => T): T = { - val jedis = jedisPool.getResource() - try { - f(jedis) - } finally { - jedis.close() - } + protected final def withJedis[T](f: JClient => F[T]): F[T] = { + Resource.fromAutoCloseable(F.delay(jedisPool.getResource())).use(jedis => F.suspend(f(jedis))) } } diff --git a/modules/redis/src/main/scala/scalacache/redis/RedisClusterCache.scala b/modules/redis/src/main/scala/scalacache/redis/RedisClusterCache.scala index 3698f7a6..670c2ec9 100644 --- a/modules/redis/src/main/scala/scalacache/redis/RedisClusterCache.scala +++ b/modules/redis/src/main/scala/scalacache/redis/RedisClusterCache.scala @@ -5,17 +5,22 @@ import redis.clients.jedis.exceptions.JedisClusterException import scalacache.logging.Logger import scalacache.redis.StringEnrichment._ import scalacache.serialization.Codec -import scalacache.{AbstractCache, CacheConfig, Mode} +import scalacache.{AbstractCache, CacheConfig} import scala.concurrent.duration.{Duration, _} -import scala.language.higherKinds +import cats.implicits._ +import cats.effect.Sync -class RedisClusterCache[V](val jedisCluster: JedisCluster)(implicit val config: CacheConfig, val codec: Codec[V]) - extends AbstractCache[V] { +class RedisClusterCache[F[_]: Sync, V](val jedisCluster: JedisCluster)( + implicit val config: CacheConfig, + val codec: Codec[V] +) extends AbstractCache[F, V] { + + protected def F: Sync[F] = Sync[F] override protected final val logger = Logger.getLogger(getClass.getName) - override protected def doGet[F[_]](key: String)(implicit mode: Mode[F]): F[Option[V]] = mode.M.suspend { + override protected def doGet(key: String): F[Option[V]] = F.suspend { val bytes = jedisCluster.get(key.utf8bytes) val result: Codec.DecodingResult[Option[V]] = { if (bytes != null) @@ -23,36 +28,35 @@ class RedisClusterCache[V](val jedisCluster: JedisCluster)(implicit val config: else Right(None) } + result match { case Left(e) => - mode.M.raiseError(e) + F.raiseError[Option[V]](e) case Right(maybeValue) => - logCacheHitOrMiss(key, maybeValue) - mode.M.pure(maybeValue) + logCacheHitOrMiss(key, maybeValue).as(maybeValue) } } - override protected def doPut[F[_]](key: String, value: V, ttl: Option[Duration])(implicit mode: Mode[F]): F[Any] = { - mode.M.delay { + override protected def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] = { + F.delay { val keyBytes = key.utf8bytes val valueBytes = codec.encode(value) ttl match { - case None => jedisCluster.set(keyBytes, valueBytes) - case Some(Duration.Zero) => jedisCluster.set(keyBytes, valueBytes) + case None => F.delay(jedisCluster.set(keyBytes, valueBytes)) + case Some(Duration.Zero) => F.delay(jedisCluster.set(keyBytes, valueBytes)) case Some(d) if d < 1.second => - if (logger.isWarnEnabled) { + logger.ifWarnEnabled { logger.warn( s"Because Redis (pre 2.6.12) does not support sub-second expiry, TTL of $d will be rounded up to 1 second" ) - } - jedisCluster.setex(keyBytes, 1, valueBytes) + } *> F.delay(jedisCluster.setex(keyBytes, 1, valueBytes)) case Some(d) => - jedisCluster.setex(keyBytes, d.toSeconds.toInt, valueBytes) + F.delay(jedisCluster.setex(keyBytes, d.toSeconds.toInt, valueBytes)) } } } - override protected def doRemove[F[_]](key: String)(implicit mode: Mode[F]): F[Any] = mode.M.delay { + override protected def doRemove(key: String): F[Unit] = F.delay { jedisCluster.del(key.utf8bytes) } @@ -60,9 +64,9 @@ class RedisClusterCache[V](val jedisCluster: JedisCluster)(implicit val config: "JedisCluster doesn't support this operation, scheduled to be removed with the next jedis major release", "0.28.0" ) - override protected def doRemoveAll[F[_]]()(implicit mode: Mode[F]): F[Any] = mode.M.raiseError { + override protected def doRemoveAll: F[Unit] = F.raiseError { new JedisClusterException("No way to dispatch this command to Redis Cluster.") } - override def close[F[_]]()(implicit mode: Mode[F]): F[Any] = mode.M.delay(jedisCluster.close()) + override val close: F[Unit] = F.delay(jedisCluster.close()) } diff --git a/modules/redis/src/main/scala/scalacache/redis/SentinelRedisCache.scala b/modules/redis/src/main/scala/scalacache/redis/SentinelRedisCache.scala index ac56db87..6d7ceb3e 100644 --- a/modules/redis/src/main/scala/scalacache/redis/SentinelRedisCache.scala +++ b/modules/redis/src/main/scala/scalacache/redis/SentinelRedisCache.scala @@ -4,26 +4,27 @@ import org.apache.commons.pool2.impl.GenericObjectPoolConfig import redis.clients.jedis._ import scala.collection.JavaConverters._ -import scala.language.higherKinds -import scalacache.{CacheConfig, Mode} +import scalacache.{CacheConfig} import scalacache.serialization.Codec +import cats.implicits._ +import cats.effect.Sync /** * Thin wrapper around Jedis that works with Redis Sentinel. */ -class SentinelRedisCache[V](val jedisPool: JedisSentinelPool)(implicit val config: CacheConfig, val codec: Codec[V]) - extends RedisCacheBase[V] { +class SentinelRedisCache[F[_]: Sync, V](val jedisPool: JedisSentinelPool)( + implicit val config: CacheConfig, + val codec: Codec[V] +) extends RedisCacheBase[F, V] { + + protected def F: Sync[F] = Sync[F] type JClient = Jedis - protected def doRemoveAll[F[_]]()(implicit mode: Mode[F]): F[Any] = mode.M.delay { - val jedis = jedisPool.getResource() - try { - jedis.flushDB() - } finally { - jedis.close() + protected def doRemoveAll: F[Unit] = + withJedis { jedis => + F.delay(jedis.flushDB()) } - } } @@ -36,10 +37,10 @@ object SentinelRedisCache { * @param sentinels set of sentinels in format [host1:port, host2:port] * @param password password of the cluster */ - def apply[V](clusterName: String, sentinels: Set[String], password: String)( + def apply[F[_]: Sync, V](clusterName: String, sentinels: Set[String], password: String)( implicit config: CacheConfig, codec: Codec[V] - ): SentinelRedisCache[V] = + ): SentinelRedisCache[F, V] = apply(new JedisSentinelPool(clusterName, sentinels.asJava, new GenericObjectPoolConfig, password)) /** @@ -50,10 +51,15 @@ object SentinelRedisCache { * @param password password of the cluster * @param poolConfig config of the underlying pool */ - def apply[V](clusterName: String, sentinels: Set[String], poolConfig: GenericObjectPoolConfig, password: String)( + def apply[F[_]: Sync, V]( + clusterName: String, + sentinels: Set[String], + poolConfig: GenericObjectPoolConfig, + password: String + )( implicit config: CacheConfig, codec: Codec[V] - ): SentinelRedisCache[V] = + ): SentinelRedisCache[F, V] = apply(new JedisSentinelPool(clusterName, sentinels.asJava, poolConfig, password)) /** @@ -61,9 +67,9 @@ object SentinelRedisCache { * * @param jedisSentinelPool a JedisSentinelPool */ - def apply[V]( + def apply[F[_]: Sync, V]( jedisSentinelPool: JedisSentinelPool - )(implicit config: CacheConfig, codec: Codec[V]): SentinelRedisCache[V] = - new SentinelRedisCache[V](jedisSentinelPool) + )(implicit config: CacheConfig, codec: Codec[V]): SentinelRedisCache[F, V] = + new SentinelRedisCache[F, V](jedisSentinelPool) } diff --git a/modules/redis/src/main/scala/scalacache/redis/ShardedRedisCache.scala b/modules/redis/src/main/scala/scalacache/redis/ShardedRedisCache.scala index 48cc3064..bbc497f2 100644 --- a/modules/redis/src/main/scala/scalacache/redis/ShardedRedisCache.scala +++ b/modules/redis/src/main/scala/scalacache/redis/ShardedRedisCache.scala @@ -4,23 +4,25 @@ import redis.clients.jedis._ import scala.collection.JavaConverters._ import scala.language.higherKinds -import scalacache.{CacheConfig, Mode} +import scalacache.{CacheConfig} import scalacache.serialization.Codec +import cats.effect.Sync /** * Thin wrapper around Jedis that works with sharded Redis. */ -class ShardedRedisCache[V](val jedisPool: ShardedJedisPool)(implicit val config: CacheConfig, val codec: Codec[V]) - extends RedisCacheBase[V] { +class ShardedRedisCache[F[_]: Sync, V](val jedisPool: ShardedJedisPool)( + implicit val config: CacheConfig, + val codec: Codec[V] +) extends RedisCacheBase[F, V] { + + protected def F: Sync[F] = Sync[F] type JClient = ShardedJedis - protected def doRemoveAll[F[_]]()(implicit mode: Mode[F]): F[Any] = mode.M.delay { - val jedis = jedisPool.getResource() - try { + protected val doRemoveAll: F[Unit] = withJedis { jedis => + F.delay { jedis.getAllShards.asScala.foreach(_.flushDB()) - } finally { - jedis.close() } } @@ -31,7 +33,9 @@ object ShardedRedisCache { /** * Create a sharded Redis client connecting to the given hosts and use it for caching */ - def apply[V](hosts: (String, Int)*)(implicit config: CacheConfig, codec: Codec[V]): ShardedRedisCache[V] = { + def apply[F[_]: Sync, V]( + hosts: (String, Int)* + )(implicit config: CacheConfig, codec: Codec[V]): ShardedRedisCache[F, V] = { val shards = hosts.map { case (host, port) => new JedisShardInfo(host, port) } @@ -43,7 +47,9 @@ object ShardedRedisCache { * Create a cache that uses the given ShardedJedis client pool * @param jedisPool a ShardedJedis pool */ - def apply[V](jedisPool: ShardedJedisPool)(implicit config: CacheConfig, codec: Codec[V]): ShardedRedisCache[V] = - new ShardedRedisCache[V](jedisPool) + def apply[F[_]: Sync, V]( + jedisPool: ShardedJedisPool + )(implicit config: CacheConfig, codec: Codec[V]): ShardedRedisCache[F, V] = + new ShardedRedisCache[F, V](jedisPool) } diff --git a/modules/tests/src/test/scala/integrationtests/IntegrationTests.scala b/modules/tests/src/test/scala/integrationtests/IntegrationTests.scala index ece16015..8f93cf26 100644 --- a/modules/tests/src/test/scala/integrationtests/IntegrationTests.scala +++ b/modules/tests/src/test/scala/integrationtests/IntegrationTests.scala @@ -4,7 +4,6 @@ import java.util.UUID import org.scalatest._ import cats.effect.{IO => CatsIO} -import scalaz.concurrent.{Task => ScalazTask} import net.spy.memcached.{AddrUtil, MemcachedClient} import redis.clients.jedis.JedisPool @@ -16,9 +15,12 @@ import scalacache._ import scalacache.caffeine.CaffeineCache import scalacache.memcached.MemcachedCache import scalacache.redis.RedisCache +import cats.effect.Clock class IntegrationTests extends FlatSpec with Matchers with BeforeAndAfterAll { + implicit val catsClock: Clock[CatsIO] = Clock.create + private val memcachedClient = new MemcachedClient(AddrUtil.getAddresses("localhost:11211")) private val jedisPool = new JedisPool("localhost", 6379) @@ -48,18 +50,18 @@ class IntegrationTests extends FlatSpec with Matchers with BeforeAndAfterAll { } } - case class CacheBackend(name: String, cache: Cache[String]) + case class CacheBackend(name: String, cache: Cache[CatsIO, String]) - private val caffeine = CacheBackend("Caffeine", CaffeineCache[String]) + private val caffeine = CacheBackend("Caffeine", CaffeineCache[CatsIO, String].unsafeRunSync()) private val memcached: Seq[CacheBackend] = if (memcachedIsRunning) { Seq( { import scalacache.serialization.binary._ - CacheBackend("(Memcached) ⇔ (binary codec)", MemcachedCache[String](memcachedClient)) + CacheBackend("(Memcached) ⇔ (binary codec)", MemcachedCache[CatsIO, String](memcachedClient)) }, { import scalacache.serialization.circe._ - CacheBackend("(Memcached) ⇔ (circe codec)", MemcachedCache[String](memcachedClient)) + CacheBackend("(Memcached) ⇔ (circe codec)", MemcachedCache[CatsIO, String](memcachedClient)) } ) } else { @@ -72,10 +74,10 @@ class IntegrationTests extends FlatSpec with Matchers with BeforeAndAfterAll { Seq( { import scalacache.serialization.binary._ - CacheBackend("(Redis) ⇔ (binary codec)", RedisCache[String](jedisPool)) + CacheBackend("(Redis) ⇔ (binary codec)", RedisCache[CatsIO, String](jedisPool)) }, { import scalacache.serialization.circe._ - CacheBackend("(Redis) ⇔ (circe codec)", RedisCache[String](jedisPool)) + CacheBackend("(Redis) ⇔ (circe codec)", RedisCache[CatsIO, String](jedisPool)) } ) else { @@ -88,8 +90,7 @@ class IntegrationTests extends FlatSpec with Matchers with BeforeAndAfterAll { for (CacheBackend(name, cache) <- backends) { s"$name ⇔ (cats-effect IO)" should "defer the computation and give the correct result" in { - implicit val theCache: Cache[String] = cache - implicit val mode: Mode[CatsIO] = CatsEffect.modes.async + implicit val theCache: Cache[CatsIO, String] = cache val key = UUID.randomUUID().toString val initialValue = UUID.randomUUID().toString @@ -109,35 +110,11 @@ class IntegrationTests extends FlatSpec with Matchers with BeforeAndAfterAll { val result: Option[String] = program.unsafeRunSync() assert(result.contains("prepended " + initialValue)) } - - s"$name ⇔ (Scalaz Task)" should "defer the computation and give the correct result" in { - implicit val theCache: Cache[String] = cache - implicit val mode: Mode[ScalazTask] = Scalaz72.modes.task - - val key = UUID.randomUUID().toString - val initialValue = UUID.randomUUID().toString - - val program = - for { - _ <- put(key)(initialValue) - readFromCache <- get(key) - updatedValue = "prepended " + readFromCache.getOrElse("couldn't find in cache!") - _ <- put(key)(updatedValue) - finalValueFromCache <- get(key) - } yield finalValueFromCache - - checkComputationHasNotRun(key) - - val result: Option[String] = program.unsafePerformSync - assert(result.contains("prepended " + initialValue)) - } - } - private def checkComputationHasNotRun(key: String)(implicit cache: Cache[String]): Unit = { + private def checkComputationHasNotRun(key: String)(implicit cache: Cache[CatsIO, String]): Unit = { Thread.sleep(1000) - implicit val mode: Mode[Id] = scalacache.modes.sync.mode - assert(scalacache.sync.get(key).isEmpty) + assert(cache.get(key).unsafeRunSync.isEmpty) } } From 3365f9022bff404c2babeea360c51182c5b73dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 31 May 2020 19:14:21 +0200 Subject: [PATCH 12/21] Fix integration tests --- .../scalacache/caffeine/CaffeineCache.scala | 20 +++++++++---------- .../src/main/scala/scalacache/Entry.scala | 19 ++++++++++++------ 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala b/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala index 747d7126..bc40bb18 100644 --- a/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala +++ b/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala @@ -19,7 +19,7 @@ import cats.MonadError * * This cache implementation is synchronous. */ -class CaffeineCache[F[_]: Sync, V](val underlying: CCache[String, Entry[F, V]])( +class CaffeineCache[F[_]: Sync, V](val underlying: CCache[String, Entry[V]])( implicit val config: CacheConfig, clock: Clock[F] ) extends AbstractCache[F, V] { @@ -31,20 +31,20 @@ class CaffeineCache[F[_]: Sync, V](val underlying: CCache[String, Entry[F, V]])( F.delay { Option(underlying.getIfPresent(key)) } - .flatMap(_.filterA(_.isExpired)) + .flatMap(_.filterA(Entry.isExpired[F, V])) .map(_.map(_.value)) .flatTap { result => logCacheHitOrMiss(key, result) } } - def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] = ttl.traverse(toExpiryTime).flatMap { expiry => - F.delay { - val entry = Entry[F, V](value, expiry) - underlying.put(key, entry) - logCachePut(key, ttl) + def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] = + ttl.traverse(toExpiryTime).flatMap { expiry => + F.delay { + val entry = Entry(value, expiry) + underlying.put(key, entry) + } *> logCachePut(key, ttl) } - } override def doRemove(key: String): F[Unit] = F.delay(underlying.invalidate(key)) @@ -68,7 +68,7 @@ object CaffeineCache { * Create a new Caffeine cache. */ def apply[F[_]: Sync: Clock, V](implicit config: CacheConfig): F[CaffeineCache[F, V]] = - Sync[F].delay(Caffeine.newBuilder().build[String, Entry[F, V]]()).map(apply(_)) + Sync[F].delay(Caffeine.newBuilder().build[String, Entry[V]]()).map(apply(_)) /** * Create a new cache utilizing the given underlying Caffeine cache. @@ -76,7 +76,7 @@ object CaffeineCache { * @param underlying a Caffeine cache */ def apply[F[_]: Sync: Clock, V]( - underlying: CCache[String, Entry[F, V]] + underlying: CCache[String, Entry[V]] )(implicit config: CacheConfig): CaffeineCache[F, V] = new CaffeineCache(underlying) diff --git a/modules/core/shared/src/main/scala/scalacache/Entry.scala b/modules/core/shared/src/main/scala/scalacache/Entry.scala index f153872b..2f845eda 100644 --- a/modules/core/shared/src/main/scala/scalacache/Entry.scala +++ b/modules/core/shared/src/main/scala/scalacache/Entry.scala @@ -10,15 +10,22 @@ import cats.Applicative /** * A cache entry with an optional expiry time */ -case class Entry[F[_], +A](value: A, expiresAt: Option[Instant]) { +case class Entry[+A](value: A, expiresAt: Option[Instant]) + +object Entry { /** * Has the entry expired yet? */ - def isExpired(implicit clock: Clock[F], applicative: Applicative[F]): F[Boolean] = - expiresAt - .traverse { ttl => - clock.monotonic(TimeUnit.MILLISECONDS).map(Instant.ofEpochMilli(_)).map(ttl.isBefore(_)) + def isExpired[F[_], A](entry: Entry[A])(implicit clock: Clock[F], applicative: Applicative[F]): F[Boolean] = + entry.expiresAt + .traverse { expiration => + val now = clock.monotonic(TimeUnit.MILLISECONDS).map(Instant.ofEpochMilli(_)) + + now.map(expiration.isBefore(_)) + } + .map { + case None | Some(true) => true + case Some(false) => false } - .map(_.contains(true)) } From 45699fa478755e354c6362110e6235f4aae1d1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 31 May 2020 19:40:26 +0200 Subject: [PATCH 13/21] Fix more caches --- .../caffeine/CaffeineCacheSpec.scala | 50 ++++++++++++++----- .../scala/scalacache/logging/Logger.scala | 27 +++++----- .../memcached/MemcachedCacheSpec.scala | 12 ++--- .../scala/scalacache/redis/Issue32Spec.scala | 11 ++-- .../scalacache/redis/RedisCacheSpec.scala | 5 +- .../scalacache/redis/RedisCacheSpecBase.scala | 23 ++++----- .../redis/RedisClusterCacheSpec.scala | 5 +- .../redis/SentinelRedisCacheSpec.scala | 5 +- .../redis/ShardedRedisCacheSpec.scala | 5 +- 9 files changed, 85 insertions(+), 58 deletions(-) diff --git a/modules/caffeine/src/test/scala/scalacache/caffeine/CaffeineCacheSpec.scala b/modules/caffeine/src/test/scala/scalacache/caffeine/CaffeineCacheSpec.scala index ae33a439..89a4d938 100644 --- a/modules/caffeine/src/test/scala/scalacache/caffeine/CaffeineCacheSpec.scala +++ b/modules/caffeine/src/test/scala/scalacache/caffeine/CaffeineCacheSpec.scala @@ -1,6 +1,6 @@ package scalacache.caffeine -import java.time.{Clock, Instant, ZoneOffset} +import java.time.{Instant, ZoneOffset} import scalacache._ import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers} @@ -8,12 +8,31 @@ import com.github.benmanes.caffeine.cache.Caffeine import scala.concurrent.duration._ import org.scalatest.concurrent.ScalaFutures +import cats.effect.SyncIO +import cats.effect.Clock +import java.util.concurrent.TimeUnit class CaffeineCacheSpec extends FlatSpec with Matchers with BeforeAndAfter with ScalaFutures { private def newCCache = Caffeine.newBuilder.build[String, Entry[String]] - import scalacache.modes.sync._ + val defaultClock = Clock.create[SyncIO] + def fixedClock(now: Instant): Clock[SyncIO] = new Clock[SyncIO] { + def realTime(unit: TimeUnit): SyncIO[Long] = SyncIO.pure { + unit.convert(now.toEpochMilli(), TimeUnit.MILLISECONDS) + } + + def monotonic(unit: concurrent.duration.TimeUnit): SyncIO[Long] = realTime(unit) + + } + + def newIOCache[V]( + underlying: com.github.benmanes.caffeine.cache.Cache[String, Entry[V]], + clock: Clock[SyncIO] = defaultClock + ) = { + implicit val clockImplicit = clock + CaffeineCache[SyncIO, V](underlying) + } behavior of "get" @@ -21,12 +40,12 @@ class CaffeineCacheSpec extends FlatSpec with Matchers with BeforeAndAfter with val underlying = newCCache val entry = Entry("hello", expiresAt = None) underlying.put("key1", entry) - CaffeineCache(underlying).get("key1") should be(Some("hello")) + newIOCache(underlying).get("key1").unsafeRunSync() should be(Some("hello")) } it should "return None if the given key does not exist in the underlying cache" in { val underlying = newCCache - CaffeineCache(underlying).get("non-existent key") should be(None) + newIOCache(underlying).get("non-existent key").unsafeRunSync() should be(None) } it should "return None if the given key exists but the value has expired" in { @@ -34,35 +53,40 @@ class CaffeineCacheSpec extends FlatSpec with Matchers with BeforeAndAfter with val expiredEntry = Entry("hello", expiresAt = Some(Instant.now.minusSeconds(1))) underlying.put("key1", expiredEntry) - CaffeineCache(underlying).get("key1") should be(None) + newIOCache(underlying).get("key1").unsafeRunSync() should be(None) } behavior of "put" it should "store the given key-value pair in the underlying cache with no TTL" in { val underlying = newCCache - CaffeineCache(underlying).put("key1")("hello", None) + newIOCache(underlying).put("key1")("hello", None).unsafeRunSync() + underlying.getIfPresent("key1") should be(Entry("hello", None)) } behavior of "put with TTL" it should "store the given key-value pair in the underlying cache with the given TTL" in { - val now = Instant.now() - val clock = Clock.fixed(now, ZoneOffset.UTC) + val now = Instant.parse("2020-05-31T12:00:00Z") + val clock = fixedClock(now) val underlying = newCCache - new CaffeineCache(underlying)(implicitly[CacheConfig], clock).put("key1")("hello", Some(10.seconds)) + newIOCache(underlying, clock).put("key1")("hello", Some(10.seconds)).unsafeRunSync() + underlying.getIfPresent("key1") should be(Entry("hello", expiresAt = Some(now.plusSeconds(10)))) } it should "support a TTL greater than Int.MaxValue millis" in { val now = Instant.parse("2015-10-01T00:00:00Z") - val clock = Clock.fixed(now, ZoneOffset.UTC) + val clock = fixedClock(now) val underlying = newCCache - new CaffeineCache(underlying)(implicitly[CacheConfig], clock).put("key1")("hello", Some(30.days)) - underlying.getIfPresent("key1") should be(Entry("hello", expiresAt = Some(Instant.parse("2015-10-31T00:00:00Z")))) + newIOCache(underlying, clock).put("key1")("hello", Some(30.days)).unsafeRunSync() + + underlying.getIfPresent("key1") should be( + Entry("hello", expiresAt = Some(Instant.parse("2015-10-31T00:00:00Z"))) + ) } behavior of "remove" @@ -73,7 +97,7 @@ class CaffeineCacheSpec extends FlatSpec with Matchers with BeforeAndAfter with underlying.put("key1", entry) underlying.getIfPresent("key1") should be(entry) - CaffeineCache(underlying).remove("key1") + newIOCache(underlying).remove("key1").unsafeRunSync() underlying.getIfPresent("key1") should be(null) } diff --git a/modules/core/js/src/main/scala/scalacache/logging/Logger.scala b/modules/core/js/src/main/scala/scalacache/logging/Logger.scala index b46909e7..3659be61 100644 --- a/modules/core/js/src/main/scala/scalacache/logging/Logger.scala +++ b/modules/core/js/src/main/scala/scalacache/logging/Logger.scala @@ -1,25 +1,24 @@ package scalacache.logging import scala.collection.mutable import scala.scalajs.js.Dynamic.global +import cats.effect.Sync +import cats.effect.SyncIO +import cats.~> +import cats.implicits._ +import cats.effect.SyncEffect object Logger { - - private val loggers: mutable.Map[String, Logger] = mutable.Map.empty - - def getLogger(name: String): Logger = loggers.getOrElseUpdate(name, new Logger(name)) - + def getLogger[F[_]: Sync](name: String): Logger[F] = new Logger[F](name) } -final class Logger(name: String) { - - def isDebugEnabled: Boolean = true - - def isWarnEnabled: Boolean = true +final class Logger[F[_]: Sync](name: String) { + def ifDebugEnabled[A](fa: => F[A]): F[Option[A]] = fa.map(_.some) - def debug(message: String): Unit = - global.console.debug(s"$name: $message") + def ifWarnEnabled[A](fa: => F[A]): F[Option[A]] = fa.map(_.some) - def warn(message: String, e: Throwable): Unit = - global.console.warn(s"$name: $message. Exception: $e") + def debug(message: String): F[Unit] = + Sync[F].delay(global.console.debug(s"$name: $message")) + def warn(message: String, e: Throwable): F[Unit] = + Sync[F].delay(global.console.warn(s"$name: $message. Exception: $e")) } diff --git a/modules/memcached/src/test/scala/scalacache/memcached/MemcachedCacheSpec.scala b/modules/memcached/src/test/scala/scalacache/memcached/MemcachedCacheSpec.scala index d2b7800e..1d22e891 100644 --- a/modules/memcached/src/test/scala/scalacache/memcached/MemcachedCacheSpec.scala +++ b/modules/memcached/src/test/scala/scalacache/memcached/MemcachedCacheSpec.scala @@ -9,6 +9,7 @@ import org.scalatest.time.{Span, Seconds} import scala.language.postfixOps import scalacache.serialization.Codec import scalacache.serialization.binary._ +import cats.effect.IO class MemcachedCacheSpec extends FlatSpec @@ -26,7 +27,6 @@ class MemcachedCacheSpec } import scala.concurrent.ExecutionContext.Implicits.global - import scalacache.modes.scalaFuture._ def memcachedIsRunning = { try { @@ -50,13 +50,13 @@ class MemcachedCacheSpec it should "return the value stored in Memcached" in { client.set("key1", 0, serialise(123)) - whenReady(MemcachedCache[Int](client).get("key1")) { + whenReady(MemcachedCache[IO, Int](client).get("key1").unsafeToFuture()) { _ should be(Some(123)) } } it should "return None if the given key does not exist in the underlying cache" in { - whenReady(MemcachedCache[Int](client).get("non-existent-key")) { + whenReady(MemcachedCache[IO, Int](client).get("non-existent-key").unsafeToFuture()) { _ should be(None) } } @@ -64,7 +64,7 @@ class MemcachedCacheSpec behavior of "put" it should "store the given key-value pair in the underlying cache" in { - whenReady(MemcachedCache[Int](client).put("key2")(123, None)) { _ => + whenReady(MemcachedCache[IO, Int](client).put("key2")(123, None).unsafeToFuture()) { _ => client.get("key2") should be(serialise(123)) } } @@ -72,7 +72,7 @@ class MemcachedCacheSpec behavior of "put with TTL" it should "store the given key-value pair in the underlying cache" in { - whenReady(MemcachedCache[Int](client).put("key3")(123, Some(3 seconds))) { _ => + whenReady(MemcachedCache[IO, Int](client).put("key3")(123, Some(3.seconds)).unsafeToFuture()) { _ => client.get("key3") should be(serialise(123)) // Should expire after 3 seconds @@ -88,7 +88,7 @@ class MemcachedCacheSpec client.set("key1", 0, 123) client.get("key1") should be(123) - whenReady(MemcachedCache[Int](client).remove("key1")) { _ => + whenReady(MemcachedCache[IO, Int](client).remove("key1").unsafeToFuture()) { _ => client.get("key1") should be(null) } } diff --git a/modules/redis/src/test/scala/scalacache/redis/Issue32Spec.scala b/modules/redis/src/test/scala/scalacache/redis/Issue32Spec.scala index 8e33fe90..d1f556f7 100644 --- a/modules/redis/src/test/scala/scalacache/redis/Issue32Spec.scala +++ b/modules/redis/src/test/scala/scalacache/redis/Issue32Spec.scala @@ -3,8 +3,8 @@ package scalacache.redis import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers} import scalacache.memoization._ -import scalacache.modes.sync._ import scalacache.serialization.binary._ +import cats.effect.IO case class User(id: Int, name: String) @@ -15,11 +15,12 @@ case class User(id: Int, name: String) class Issue32Spec extends FlatSpec with Matchers with BeforeAndAfter with RedisTestUtil { assumingRedisIsRunning { (pool, client) => - implicit val cache = RedisCache[List[User]](pool) + implicit val cache = RedisCache[IO, List[User]](pool) - def getUser(id: Int): List[User] = memoizeSync(None) { - List(User(id, "Taro")) - } + def getUser(id: Int): List[User] = + memoize(None) { + List(User(id, "Taro")) + }.unsafeRunSync() "memoize and Redis" should "work with List[User]" in { getUser(1) should be(List(User(1, "Taro"))) diff --git a/modules/redis/src/test/scala/scalacache/redis/RedisCacheSpec.scala b/modules/redis/src/test/scala/scalacache/redis/RedisCacheSpec.scala index a9cf1faf..4709a4df 100644 --- a/modules/redis/src/test/scala/scalacache/redis/RedisCacheSpec.scala +++ b/modules/redis/src/test/scala/scalacache/redis/RedisCacheSpec.scala @@ -5,6 +5,7 @@ import scalacache._ import scalacache.serialization.Codec import scala.language.postfixOps +import cats.effect.IO class RedisCacheSpec extends RedisCacheSpecBase with RedisTestUtil { @@ -13,8 +14,8 @@ class RedisCacheSpec extends RedisCacheSpecBase with RedisTestUtil { val withJedis = assumingRedisIsRunning _ - def constructCache[V](pool: JPool)(implicit codec: Codec[V]): CacheAlg[V] = - new RedisCache[V](jedisPool = pool) + def constructCache[V](pool: JPool)(implicit codec: Codec[V]): CacheAlg[IO, V] = + new RedisCache[IO, V](jedisPool = pool) def flushRedis(client: JClient): Unit = client.underlying.flushDB() diff --git a/modules/redis/src/test/scala/scalacache/redis/RedisCacheSpecBase.scala b/modules/redis/src/test/scala/scalacache/redis/RedisCacheSpecBase.scala index d8c82ed2..589151c6 100644 --- a/modules/redis/src/test/scala/scalacache/redis/RedisCacheSpecBase.scala +++ b/modules/redis/src/test/scala/scalacache/redis/RedisCacheSpecBase.scala @@ -12,6 +12,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.concurrent.duration._ import scala.language.postfixOps +import cats.effect.IO trait RedisCacheSpecBase extends FlatSpec @@ -34,13 +35,11 @@ trait RedisCacheSpecBase } def withJedis: ((JPool, JClient) => Unit) => Unit - def constructCache[V](pool: JPool)(implicit codec: Codec[V]): CacheAlg[V] + def constructCache[V](pool: JPool)(implicit codec: Codec[V]): CacheAlg[IO, V] def flushRedis(client: JClient): Unit def runTestsIfPossible() = { - import scalacache.modes.scalaFuture._ - withJedis { (pool, client) => val cache = constructCache[Int](pool) val failingCache = constructCache[AlwaysFailing.type](pool) @@ -53,16 +52,16 @@ trait RedisCacheSpecBase it should "return the value stored in Redis" in { client.set(bytes("key1"), serialize(123)) - whenReady(cache.get("key1")) { _ should be(Some(123)) } + whenReady(cache.get("key1").unsafeToFuture()) { _ should be(Some(123)) } } it should "return None if the given key does not exist in the underlying cache" in { - whenReady(cache.get("non-existent-key")) { _ should be(None) } + whenReady(cache.get("non-existent-key").unsafeToFuture()) { _ should be(None) } } it should "raise an error if decoding fails" in { client.set(bytes("key1"), serialize(123)) - whenReady(failingCache.get("key1").failed) { t => + whenReady(failingCache.get("key1").unsafeToFuture().failed) { t => inside(t) { case FailedToDecode(e) => e.getMessage should be("Failed to decode") } } } @@ -70,7 +69,7 @@ trait RedisCacheSpecBase behavior of "put" it should "store the given key-value pair in the underlying cache" in { - whenReady(cache.put("key2")(123, None)) { _ => + whenReady(cache.put("key2")(123, None).unsafeToFuture()) { _ => deserialize[Int](client.get(bytes("key2"))) should be(Right(123)) } } @@ -78,7 +77,7 @@ trait RedisCacheSpecBase behavior of "put with TTL" it should "store the given key-value pair in the underlying cache" in { - whenReady(cache.put("key3")(123, Some(1 second))) { _ => + whenReady(cache.put("key3")(123, Some(1 second)).unsafeToFuture()) { _ => deserialize[Int](client.get(bytes("key3"))) should be(Right(123)) // Should expire after 1 second @@ -91,7 +90,7 @@ trait RedisCacheSpecBase behavior of "put with TTL of zero" it should "store the given key-value pair in the underlying cache with no expiry" in { - whenReady(cache.put("key4")(123, Some(Duration.Zero))) { _ => + whenReady(cache.put("key4")(123, Some(Duration.Zero)).unsafeToFuture()) { _ => deserialize[Int](client.get(bytes("key4"))) should be(Right(123)) client.ttl(bytes("key4")) should be(-1L) } @@ -100,7 +99,7 @@ trait RedisCacheSpecBase behavior of "put with TTL of less than 1 second" it should "store the given key-value pair in the underlying cache" in { - whenReady(cache.put("key5")(123, Some(100 milliseconds))) { _ => + whenReady(cache.put("key5")(123, Some(100 milliseconds)).unsafeToFuture()) { _ => deserialize[Int](client.get(bytes("key5"))) should be(Right(123)) client.pttl("key5").toLong should be > 0L @@ -115,7 +114,7 @@ trait RedisCacheSpecBase def roundTrip[V](key: String, value: V)(implicit codec: Codec[V]): Future[Option[V]] = { val c = constructCache[V](pool) - c.put(key)(value, None).flatMap(_ => c.get(key)) + c.put(key)(value, None).flatMap(_ => c.get(key)).unsafeToFuture() } it should "round-trip a String" in { @@ -151,7 +150,7 @@ trait RedisCacheSpecBase client.set(bytes("key1"), serialize(123)) deserialize[Int](client.get(bytes("key1"))) should be(Right(123)) - whenReady(cache.remove("key1")) { _ => + whenReady(cache.remove("key1").unsafeToFuture()) { _ => client.get("key1") should be(null) } } diff --git a/modules/redis/src/test/scala/scalacache/redis/RedisClusterCacheSpec.scala b/modules/redis/src/test/scala/scalacache/redis/RedisClusterCacheSpec.scala index cfcb6ef6..e1b30012 100644 --- a/modules/redis/src/test/scala/scalacache/redis/RedisClusterCacheSpec.scala +++ b/modules/redis/src/test/scala/scalacache/redis/RedisClusterCacheSpec.scala @@ -7,6 +7,7 @@ import scalacache.serialization.Codec import scala.collection.JavaConverters._ import scala.language.postfixOps import scala.util.{Failure, Success, Try} +import cats.effect.IO class RedisClusterCacheSpec extends RedisCacheSpecBase with RedisTestUtil { @@ -15,8 +16,8 @@ class RedisClusterCacheSpec extends RedisCacheSpecBase with RedisTestUtil { override val withJedis = assumingRedisClusterIsRunning _ - def constructCache[V](jedisCluster: JedisCluster)(implicit codec: Codec[V]): CacheAlg[V] = - new RedisClusterCache[V](jedisCluster) + def constructCache[V](jedisCluster: JedisCluster)(implicit codec: Codec[V]): CacheAlg[IO, V] = + new RedisClusterCache[IO, V](jedisCluster) def flushRedis(client: JClient): Unit = client.underlying.getClusterNodes.asScala.mapValues(_.getResource.flushDB()) diff --git a/modules/redis/src/test/scala/scalacache/redis/SentinelRedisCacheSpec.scala b/modules/redis/src/test/scala/scalacache/redis/SentinelRedisCacheSpec.scala index 93ed800b..5571a2c7 100644 --- a/modules/redis/src/test/scala/scalacache/redis/SentinelRedisCacheSpec.scala +++ b/modules/redis/src/test/scala/scalacache/redis/SentinelRedisCacheSpec.scala @@ -7,6 +7,7 @@ import scala.collection.JavaConverters._ import scala.util.{Failure, Success, Try} import scalacache._ import scalacache.serialization.Codec +import cats.effect.IO class SentinelRedisCacheSpec extends RedisCacheSpecBase { @@ -15,8 +16,8 @@ class SentinelRedisCacheSpec extends RedisCacheSpecBase { val withJedis = assumingRedisSentinelIsRunning _ - def constructCache[V](pool: JPool)(implicit codec: Codec[V]): CacheAlg[V] = - new SentinelRedisCache[V](jedisPool = pool) + def constructCache[V](pool: JPool)(implicit codec: Codec[V]): CacheAlg[IO, V] = + new SentinelRedisCache[IO, V](jedisPool = pool) def flushRedis(client: JClient): Unit = client.underlying.flushDB() diff --git a/modules/redis/src/test/scala/scalacache/redis/ShardedRedisCacheSpec.scala b/modules/redis/src/test/scala/scalacache/redis/ShardedRedisCacheSpec.scala index 3f6ac02d..a9bc2731 100644 --- a/modules/redis/src/test/scala/scalacache/redis/ShardedRedisCacheSpec.scala +++ b/modules/redis/src/test/scala/scalacache/redis/ShardedRedisCacheSpec.scala @@ -6,6 +6,7 @@ import scalacache.serialization.Codec import scala.collection.JavaConverters._ import scala.util.{Failure, Success, Try} +import cats.effect.IO class ShardedRedisCacheSpec extends RedisCacheSpecBase { @@ -14,8 +15,8 @@ class ShardedRedisCacheSpec extends RedisCacheSpecBase { val withJedis = assumingMultipleRedisAreRunning _ - def constructCache[V](pool: JPool)(implicit codec: Codec[V]): CacheAlg[V] = - new ShardedRedisCache[V](jedisPool = pool) + def constructCache[V](pool: JPool)(implicit codec: Codec[V]): CacheAlg[IO, V] = + new ShardedRedisCache[IO, V](jedisPool = pool) def flushRedis(client: JClient): Unit = client.underlying.getAllShards.asScala.foreach(_.flushDB()) From 81fa509990c889dd2eed672bd58fe6e3c672ed1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 31 May 2020 19:42:32 +0200 Subject: [PATCH 14/21] Remove cats-effect module --- build.sbt | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/build.sbt b/build.sbt index 43dc6b10..ec679582 100644 --- a/build.sbt +++ b/build.sbt @@ -29,7 +29,6 @@ lazy val root: Project = Project(id = "scalacache", base = file(".")) memcached, redis, caffeine, - catsEffect, circe, tests ) @@ -96,15 +95,6 @@ lazy val caffeine = jvmOnlyModule("caffeine") coverageFailOnMinimum := true ) -lazy val catsEffect = jvmOnlyModule("cats-effect") - .settings( - libraryDependencies ++= Seq( - "org.typelevel" %% "cats-effect" % "2.0.0" - ), - coverageMinimum := 50, - coverageFailOnMinimum := true - ) - def circeVersion(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { case Some((2, scalaMajor)) if scalaMajor >= 12 => "0.13.0" @@ -127,7 +117,7 @@ lazy val circe = jvmOnlyModule("circe") lazy val tests = jvmOnlyModule("tests") .settings(publishArtifact := false) - .dependsOn(caffeine, memcached, redis, catsEffect, circe) + .dependsOn(caffeine, memcached, redis, circe) lazy val docs = jvmOnlyModule("docs") .enablePlugins(MicrositesPlugin) @@ -151,7 +141,6 @@ lazy val docs = jvmOnlyModule("docs") memcached, redis, caffeine, - catsEffect, circe ) From 4f2d350b6d0c723a4d16635d3916a923ccdfeb94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 31 May 2020 19:45:43 +0200 Subject: [PATCH 15/21] Fix benchmarks and JS compilation --- build.sbt | 2 +- .../benchmark/CaffeineBenchmark.scala | 20 +++++++++++-------- .../benchmark/ProfilingMemoize.scala | 15 ++++++++------ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/build.sbt b/build.sbt index ec679582..c8418323 100644 --- a/build.sbt +++ b/build.sbt @@ -40,7 +40,7 @@ lazy val core = moduleName := "scalacache-core", libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value, - "org.typelevel" %% "cats-effect" % "2.1.3", + "org.typelevel" %%% "cats-effect" % "2.1.3", "org.scalatest" %%% "scalatest" % "3.0.8" % Test, "org.scalacheck" %%% "scalacheck" % "1.14.3" % Test ), diff --git a/modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala b/modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala index 8fd77cc6..c2d6eac3 100644 --- a/modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala +++ b/modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala @@ -9,24 +9,28 @@ import com.github.benmanes.caffeine.cache.Caffeine import scalacache._ import caffeine._ import memoization._ -import scalacache.modes.sync._ +import cats.effect.SyncIO +import cats.effect.Clock @State(Scope.Thread) class CaffeineBenchmark { - val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]() - implicit val cache: Cache[String] = CaffeineCache(underlyingCache) + implicit val clockSyncIO = Clock.create[SyncIO] + + val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]() + implicit val cache: Cache[SyncIO, String] = CaffeineCache[SyncIO, String](underlyingCache) val key = "key" val value: String = "value" - def itemCachedNoMemoize(key: String): Id[Option[String]] = { - cache.get(key) + def itemCachedNoMemoize(key: String): Option[String] = { + cache.get(key).unsafeRunSync() } - def itemCachedMemoize(key: String): String = memoizeSync(None) { - value - } + def itemCachedMemoize(key: String): String = + memoize(None) { + value + }.unsafeRunSync() // populate the cache cache.put(key)(value) diff --git a/modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala b/modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala index d3836375..54101cef 100644 --- a/modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala +++ b/modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala @@ -5,22 +5,25 @@ import com.github.benmanes.caffeine.cache.Caffeine import scalacache._ import scalacache.caffeine._ import scalacache.memoization._ -import scalacache.modes.sync._ +import cats.effect.SyncIO +import cats.effect.Clock /** * Just runs forever, endlessly calling memoize, so Java Flight Recorder can output sampling data. */ object ProfilingMemoize extends App { - val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]() - implicit val cache = CaffeineCache[String](underlyingCache) + implicit val clockSyncIO = Clock.create[SyncIO] + val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]() + implicit val cache = CaffeineCache[SyncIO, String](underlyingCache) val key = "key" val value: String = "value" - def itemCachedMemoize(key: String): String = memoizeSync(None) { - value - } + def itemCachedMemoize(key: String): String = + memoize(None) { + value + }.unsafeRunSync() var result: String = _ var i = 0L From 997ed1a0dfb9454ac9fc3d1a872d2e4c3a11b3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 31 May 2020 19:50:34 +0200 Subject: [PATCH 16/21] 2.11 isn't supported by cats anymore --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dbdb8f34..484fde0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ sudo: false language: scala scala: - - 2.11.12 - 2.13.1 - 2.12.10 jdk: From 869119962be1ccd46189f397c50506563dd29eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 31 May 2020 19:51:43 +0200 Subject: [PATCH 17/21] Remove 2.11-specific config --- build.sbt | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/build.sbt b/build.sbt index c8418323..9d7ffbdc 100644 --- a/build.sbt +++ b/build.sbt @@ -50,10 +50,6 @@ lazy val core = .jvmSettings( libraryDependencies ++= Seq( "org.slf4j" % "slf4j-api" % "1.7.30" - ), - scala211OnlyDeps( - "org.squeryl" %% "squeryl" % "0.9.15" % Test, - "com.h2database" % "h2" % "1.4.200" % Test ) ) @@ -95,20 +91,12 @@ lazy val caffeine = jvmOnlyModule("caffeine") coverageFailOnMinimum := true ) -def circeVersion(scalaVersion: String) = - CrossVersion.partialVersion(scalaVersion) match { - case Some((2, scalaMajor)) if scalaMajor >= 12 => "0.13.0" - case Some((2, scalaMajor)) if scalaMajor >= 11 => "0.11.1" - case _ => - throw new IllegalArgumentException(s"Unsupported Scala version $scalaVersion") - } - lazy val circe = jvmOnlyModule("circe") .settings( libraryDependencies ++= Seq( - "io.circe" %% "circe-core" % circeVersion(scalaVersion.value), - "io.circe" %% "circe-parser" % circeVersion(scalaVersion.value), - "io.circe" %% "circe-generic" % circeVersion(scalaVersion.value) % Test, + "io.circe" %% "circe-core" % "0.13.0", + "io.circe" %% "circe-parser" % "0.13.0", + "io.circe" %% "circe-generic" % "0.13.0" % Test, scalacheck ), coverageMinimum := 80, @@ -181,9 +169,3 @@ lazy val mavenSettings = Seq( false } ) - -def scala211OnlyDeps(moduleIDs: ModuleID*) = - libraryDependencies ++= (scalaBinaryVersion.value match { - case "2.11" => moduleIDs - case other => Nil - }) From d7727299d5f8c1dd95e8bdd2b6e8c15070c099a8 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Fri, 3 Jul 2020 07:22:40 -0600 Subject: [PATCH 18/21] remove extra in RedisClusterCache#doPut --- .../scalacache/redis/RedisClusterCache.scala | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/modules/redis/src/main/scala/scalacache/redis/RedisClusterCache.scala b/modules/redis/src/main/scala/scalacache/redis/RedisClusterCache.scala index 670c2ec9..d98bddd9 100644 --- a/modules/redis/src/main/scala/scalacache/redis/RedisClusterCache.scala +++ b/modules/redis/src/main/scala/scalacache/redis/RedisClusterCache.scala @@ -38,21 +38,19 @@ class RedisClusterCache[F[_]: Sync, V](val jedisCluster: JedisCluster)( } override protected def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] = { - F.delay { - val keyBytes = key.utf8bytes - val valueBytes = codec.encode(value) - ttl match { - case None => F.delay(jedisCluster.set(keyBytes, valueBytes)) - case Some(Duration.Zero) => F.delay(jedisCluster.set(keyBytes, valueBytes)) - case Some(d) if d < 1.second => - logger.ifWarnEnabled { - logger.warn( - s"Because Redis (pre 2.6.12) does not support sub-second expiry, TTL of $d will be rounded up to 1 second" - ) - } *> F.delay(jedisCluster.setex(keyBytes, 1, valueBytes)) - case Some(d) => - F.delay(jedisCluster.setex(keyBytes, d.toSeconds.toInt, valueBytes)) - } + val keyBytes = key.utf8bytes + val valueBytes = codec.encode(value) + ttl match { + case None => F.delay(jedisCluster.set(keyBytes, valueBytes)) + case Some(Duration.Zero) => F.delay(jedisCluster.set(keyBytes, valueBytes)) + case Some(d) if d < 1.second => + logger.ifWarnEnabled { + logger.warn( + s"Because Redis (pre 2.6.12) does not support sub-second expiry, TTL of $d will be rounded up to 1 second" + ) + } *> F.delay(jedisCluster.setex(keyBytes, 1, valueBytes)) + case Some(d) => + F.delay(jedisCluster.setex(keyBytes, d.toSeconds.toInt, valueBytes)) } } From 4656021d120791cd488bbc25f26ccb851a73f79c Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Fri, 3 Jul 2020 08:06:15 -0600 Subject: [PATCH 19/21] update docs to get them compiling --- .../main/mdoc/docs/cache-implementations.md | 21 +++++++++----- modules/docs/src/main/mdoc/docs/flags.md | 10 +++---- modules/docs/src/main/mdoc/docs/index.md | 11 +++----- .../docs/src/main/mdoc/docs/memoization.md | 28 ++++--------------- 4 files changed, 29 insertions(+), 41 deletions(-) diff --git a/modules/docs/src/main/mdoc/docs/cache-implementations.md b/modules/docs/src/main/mdoc/docs/cache-implementations.md index d3fffd9f..96378374 100644 --- a/modules/docs/src/main/mdoc/docs/cache-implementations.md +++ b/modules/docs/src/main/mdoc/docs/cache-implementations.md @@ -19,8 +19,9 @@ Usage: import scalacache._ import scalacache.memcached._ import scalacache.serialization.binary._ +import cats.effect.IO -implicit val memcachedCache: Cache[String] = MemcachedCache("localhost:11211") +implicit val memcachedCache: Cache[IO, String] = MemcachedCache("localhost:11211") ``` or provide your own Memcached client, like this: @@ -35,7 +36,7 @@ val memcachedClient = new MemcachedClient( new BinaryConnectionFactory(), AddrUtil.getAddresses("localhost:11211") ) -implicit val customisedMemcachedCache: Cache[String] = MemcachedCache(memcachedClient) +implicit val customisedMemcachedCache: Cache[IO, String] = MemcachedCache(memcachedClient) ``` #### Keys @@ -62,8 +63,9 @@ Usage: import scalacache._ import scalacache.redis._ import scalacache.serialization.binary._ +import cats.effect.IO -implicit val redisCache: Cache[String] = RedisCache("host1", 6379) +implicit val redisCache: Cache[IO, String] = RedisCache("host1", 6379) ``` or provide your own [Jedis](https://github.com/xetorthio/jedis) client, like this: @@ -73,9 +75,10 @@ import scalacache._ import scalacache.redis._ import scalacache.serialization.binary._ import _root_.redis.clients.jedis._ +import cats.effect.IO val jedisPool = new JedisPool("localhost", 6379) -implicit val customisedRedisCache: Cache[String] = RedisCache(jedisPool) +implicit val customisedRedisCache: Cache[IO, String] = RedisCache(jedisPool) ``` ScalaCache also supports [sharded Redis](https://github.com/xetorthio/jedis/wiki/AdvancedUsage#shardedjedis) and [Redis Sentinel](http://redis.io/topics/sentinel). Just create a `ShardedRedisCache` or `SentinelRedisCache` respectively. @@ -93,8 +96,11 @@ Usage: ```scala mdoc:silent import scalacache._ import scalacache.caffeine._ +import cats.effect.{Clock, IO} -implicit val caffeineCache: Cache[String] = CaffeineCache[String] +implicit val clock: Clock[IO] = Clock.create + +implicit val caffeineCache: Cache[IO, String] = CaffeineCache[IO, String].unsafeRunSync() ``` This will build a Caffeine cache with all the default settings. If you want to customize your Caffeine cache, then build it yourself and pass it to `CaffeineCache` like this: @@ -103,13 +109,14 @@ This will build a Caffeine cache with all the default settings. If you want to c import scalacache._ import scalacache.caffeine._ import com.github.benmanes.caffeine.cache.Caffeine +import cats.effect.IO val underlyingCaffeineCache = Caffeine.newBuilder().maximumSize(10000L).build[String, Entry[String]] -implicit val customisedCaffeineCache: Cache[String] = CaffeineCache(underlyingCaffeineCache) +implicit val customisedCaffeineCache: Cache[IO, String] = CaffeineCache(underlyingCaffeineCache) ``` ```scala mdoc:invisible for (cache <- List(redisCache, customisedRedisCache, memcachedCache, customisedMemcachedCache)) { - cache.close()(scalacache.modes.sync.mode) + cache.close } ``` diff --git a/modules/docs/src/main/mdoc/docs/flags.md b/modules/docs/src/main/mdoc/docs/flags.md index 26581e64..ac9ca389 100644 --- a/modules/docs/src/main/mdoc/docs/flags.md +++ b/modules/docs/src/main/mdoc/docs/flags.md @@ -23,17 +23,17 @@ Example: import scalacache._ import scalacache.memcached._ import scalacache.memoization._ -import scalacache.modes.sync._ import scalacache.serialization.binary._ +import cats.effect.IO final case class Cat(id: Int, name: String, colour: String) -implicit val catsCache: Cache[Cat] = MemcachedCache("localhost:11211") +implicit val catsCache: Cache[IO, Cat] = MemcachedCache("localhost:11211") -def getCatWithFlags(id: Int)(implicit flags: Flags): Cat = memoizeSync(None) { +def getCatWithFlags(id: Int)(implicit flags: Flags): Cat = memoize(None) { // Do DB lookup here... Cat(id, s"cat ${id}", "black") -} +}.unsafeRunSync() def getCatMaybeSkippingCache(id: Int, skipCache: Boolean): Cat = { implicit val flags = Flags(readsEnabled = !skipCache) @@ -45,6 +45,6 @@ Tip: Because the flags are passed as a parameter to your method, they will be in ```scala mdoc:invisible for (cache <- List(catsCache)) { - cache.close()(scalacache.modes.sync.mode) + cache.close } ``` diff --git a/modules/docs/src/main/mdoc/docs/index.md b/modules/docs/src/main/mdoc/docs/index.md index 392a6c26..1bbb1b14 100644 --- a/modules/docs/src/main/mdoc/docs/index.md +++ b/modules/docs/src/main/mdoc/docs/index.md @@ -11,6 +11,7 @@ At the very least you will need to import the ScalaCache API. ```scala mdoc:silent:reset-object import scalacache._ +import cats.effect.IO ``` Note that this import also brings a bunch of useful implicit magic into scope. @@ -31,7 +32,7 @@ import scalacache.serialization.binary._ final case class Cat(id: Int, name: String, colour: String) -implicit val catsCache: Cache[Cat] = MemcachedCache("localhost:11211") +implicit val catsCache: Cache[IO, Cat] = MemcachedCache("localhost:11211") ``` Note that we made the cache `implicit` so that the ScalaCache API can find it. @@ -42,9 +43,6 @@ Note that we made the cache `implicit` so that the ScalaCache API can find it. val ericTheCat = Cat(1, "Eric", "tuxedo") val doraemon = Cat(99, "Doraemon", "blue") -// Choose the Try mode (more on this later) -import scalacache.modes.try_._ - // Add an item to the cache put("eric")(ericTheCat) @@ -70,8 +68,7 @@ caching("benjamin")(ttl = None) { // If the result of the block is wrapped in an effect, use cachingF cachingF("benjamin")(ttl = None) { - import scala.util.Try - Try { + IO.pure { // e.g. call an external API ... Cat(2, "Benjamin", "ginger") } @@ -83,6 +80,6 @@ put("foo", 123, "bar")(ericTheCat) // Will be cached with key "foo:123:bar" ```scala mdoc:invisible for (cache <- List(catsCache)) { - cache.close()(scalacache.modes.sync.mode) + cache.close } ``` diff --git a/modules/docs/src/main/mdoc/docs/memoization.md b/modules/docs/src/main/mdoc/docs/memoization.md index 2b884882..313ac493 100644 --- a/modules/docs/src/main/mdoc/docs/memoization.md +++ b/modules/docs/src/main/mdoc/docs/memoization.md @@ -12,17 +12,16 @@ import scalacache.memoization._ import scalacache.serialization.binary._ -import scalacache.modes.try_._ import scala.concurrent.duration._ -import scala.util.Try +import cats.effect.IO final case class Cat(id: Int, name: String, colour: String) -implicit val catsCache: Cache[Cat] = MemcachedCache("localhost:11211") +implicit val catsCache: Cache[IO, Cat] = MemcachedCache("localhost:11211") // You wouldn't normally need to specify the type params for memoize. // This is an artifact of the way this README is generated using tut. -def getCat(id: Int): Try[Cat] = memoize[Try, Cat](Some(10.seconds)) { +def getCat(id: Int): IO[Cat] = memoize[IO, Cat](Some(10.seconds)) { // Retrieve data from a remote API here ... Cat(id, s"cat ${id}", "black") } @@ -36,8 +35,8 @@ The next time you call the method with the same arguments the result will be ret If the result of your block is wrapped in an effect container, use `memoizeF`: ```scala mdoc -def getCatF(id: Int): Try[Cat] = memoizeF[Try, Cat](Some(10.seconds)) { - Try { +def getCatF(id: Int): IO[Cat] = memoizeF[IO, Cat](Some(10.seconds)) { + IO { // Retrieve data from a remote API here ... Cat(id, s"cat ${id}", "black") } @@ -46,21 +45,6 @@ def getCatF(id: Int): Try[Cat] = memoizeF[Try, Cat](Some(10.seconds)) { getCatF(123) ``` -#### Synchronous memoization API - -Again, there is a synchronous memoization method for convient use of the synchronous mode: - -```scala mdoc:nest -import scalacache.modes.sync._ - -def getCatSync(id: Int): Cat = memoizeSync(Some(10.seconds)) { - // Do DB lookup here ... - Cat(id, s"cat ${id}", "black") -} - -getCatSync(123) -``` - #### How it works `memoize` automatically builds a cache key based on the method being called, and the values of the arguments being passed to that method. @@ -143,6 +127,6 @@ will only include the `userId` argument's value in its cache keys. ```scala mdoc:invisible for (cache <- List(catsCache)) { - cache.close()(scalacache.modes.sync.mode) + cache.close } ``` From 3a991ba59b9d42c16e7c3fb76d5e9f6a7ab481c9 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Fri, 3 Jul 2020 08:19:15 -0600 Subject: [PATCH 20/21] fix mdoc cleanup to close caches by running cache.close --- modules/docs/src/main/mdoc/docs/cache-implementations.md | 2 +- modules/docs/src/main/mdoc/docs/flags.md | 2 +- modules/docs/src/main/mdoc/docs/index.md | 2 +- modules/docs/src/main/mdoc/docs/memoization.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/docs/src/main/mdoc/docs/cache-implementations.md b/modules/docs/src/main/mdoc/docs/cache-implementations.md index 96378374..965093dd 100644 --- a/modules/docs/src/main/mdoc/docs/cache-implementations.md +++ b/modules/docs/src/main/mdoc/docs/cache-implementations.md @@ -117,6 +117,6 @@ implicit val customisedCaffeineCache: Cache[IO, String] = CaffeineCache(underlyi ```scala mdoc:invisible for (cache <- List(redisCache, customisedRedisCache, memcachedCache, customisedMemcachedCache)) { - cache.close + cache.close.unsafeRunSync() } ``` diff --git a/modules/docs/src/main/mdoc/docs/flags.md b/modules/docs/src/main/mdoc/docs/flags.md index ac9ca389..c9d5aab5 100644 --- a/modules/docs/src/main/mdoc/docs/flags.md +++ b/modules/docs/src/main/mdoc/docs/flags.md @@ -45,6 +45,6 @@ Tip: Because the flags are passed as a parameter to your method, they will be in ```scala mdoc:invisible for (cache <- List(catsCache)) { - cache.close + cache.close.unsafeRunSync() } ``` diff --git a/modules/docs/src/main/mdoc/docs/index.md b/modules/docs/src/main/mdoc/docs/index.md index 1bbb1b14..c97a1a0d 100644 --- a/modules/docs/src/main/mdoc/docs/index.md +++ b/modules/docs/src/main/mdoc/docs/index.md @@ -80,6 +80,6 @@ put("foo", 123, "bar")(ericTheCat) // Will be cached with key "foo:123:bar" ```scala mdoc:invisible for (cache <- List(catsCache)) { - cache.close + cache.close.unsafeRunSync() } ``` diff --git a/modules/docs/src/main/mdoc/docs/memoization.md b/modules/docs/src/main/mdoc/docs/memoization.md index 313ac493..6f40dc28 100644 --- a/modules/docs/src/main/mdoc/docs/memoization.md +++ b/modules/docs/src/main/mdoc/docs/memoization.md @@ -127,6 +127,6 @@ will only include the `userId` argument's value in its cache keys. ```scala mdoc:invisible for (cache <- List(catsCache)) { - cache.close + cache.close.unsafeRunSync() } ``` From d1c29adba4697ee91e5f98c5e0efeb8c23e9bc2a Mon Sep 17 00:00:00 2001 From: Chris Birchall Date: Fri, 31 Jul 2020 16:22:54 +0100 Subject: [PATCH 21/21] Remove a couple of TODOs --- modules/core/shared/src/main/scala/scalacache/CacheAlg.scala | 2 +- modules/core/shared/src/main/scala/scalacache/package.scala | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala b/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala index 282ae40a..8a29845e 100644 --- a/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala +++ b/modules/core/shared/src/main/scala/scalacache/CacheAlg.scala @@ -7,7 +7,7 @@ import scala.language.higherKinds /** * Abstract algebra describing the operations a cache can perform * - * @tparam F the effect of the cache. //todo elaborate + * @tparam F The effect monad in which all cache operations will be performed. * @tparam V The value of types stored in the cache. */ trait CacheAlg[F[_], V] { diff --git a/modules/core/shared/src/main/scala/scalacache/package.scala b/modules/core/shared/src/main/scala/scalacache/package.scala index a8a234d2..a44e07cf 100644 --- a/modules/core/shared/src/main/scala/scalacache/package.scala +++ b/modules/core/shared/src/main/scala/scalacache/package.scala @@ -3,9 +3,6 @@ import scala.language.higherKinds package object scalacache { - //todo - type Id[X] = X - /** * Get the value corresponding to the given key from the cache. *