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: diff --git a/build.sbt b/build.sbt index c6029260..9d7ffbdc 100644 --- a/build.sbt +++ b/build.sbt @@ -29,8 +29,6 @@ lazy val root: Project = Project(id = "scalacache", base = file(".")) memcached, redis, caffeine, - catsEffect, - scalaz72, circe, tests ) @@ -42,6 +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.scalatest" %%% "scalatest" % "3.0.8" % Test, "org.scalacheck" %%% "scalacheck" % "1.14.3" % Test ), @@ -51,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 ) ) @@ -96,38 +91,12 @@ 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 - ) - -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" - 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, @@ -136,7 +105,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, circe) lazy val docs = jvmOnlyModule("docs") .enablePlugins(MicrositesPlugin) @@ -160,8 +129,6 @@ lazy val docs = jvmOnlyModule("docs") memcached, redis, caffeine, - catsEffect, - scalaz72, circe ) @@ -192,7 +159,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 ) @@ -202,9 +169,3 @@ lazy val mavenSettings = Seq( false } ) - -def scala211OnlyDeps(moduleIDs: ModuleID*) = - libraryDependencies ++= (scalaBinaryVersion.value match { - case "2.11" => moduleIDs - case other => Nil - }) diff --git a/modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala b/modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala index 7c1ad7ae..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 key = "key" + 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 3836ee62..54101cef 100644 --- a/modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala +++ b/modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala @@ -5,25 +5,28 @@ 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 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 + 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 05bc8e22..bc40bb18 100644 --- a/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala +++ b/modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala @@ -1,79 +1,83 @@ 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 /* * Thin wrapper around Caffeine. * * This cache implementation is synchronous. */ -class CaffeineCache[V](val underlying: CCache[String, Entry[V]])( +class CaffeineCache[F[_]: Sync, V](val underlying: CCache[String, Entry[V]])( implicit val config: CacheConfig, - clock: Clock = Clock.systemUTC() -) extends AbstractCache[V] { + clock: Clock[F] +) extends AbstractCache[F, V] { + protected val F: Sync[F] = Sync[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(Entry.isExpired[F, V])) + .map(_.map(_.value)) + .flatTap { result => + 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)) - 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[F[_]](key: String)(implicit mode: Mode[F]): F[Any] = - mode.M.delay(underlying.invalidate(key)) + override def doRemove(key: String): F[Unit] = + F.delay(underlying.invalidate(key)) - override def doRemoveAll[F[_]]()(implicit mode: Mode[F]): F[Any] = - mode.M.delay(underlying.invalidateAll()) + override def doRemoveAll(): F[Unit] = + F.delay(underlying.invalidateAll()) - override def close[F[_]]()(implicit mode: Mode[F]): F[Any] = { + override def close: F[Unit] = { // Nothing to do - mode.M.pure(()) + F.unit } - 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[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[V]] + )(implicit config: CacheConfig): CaffeineCache[F, V] = new CaffeineCache(underlying) } 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/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/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/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 4046269b..363a3a0b 100644 --- a/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala +++ b/modules/core/shared/src/main/scala/scalacache/AbstractCache.scala @@ -3,6 +3,11 @@ package scalacache import scala.concurrent.duration.Duration import scala.language.higherKinds +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 @@ -13,49 +18,48 @@ 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[F] { + protected implicit def F: Sync[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) - } + } else + logger + .ifDebugEnabled { + logger.debug(s"Skipping cache GET because cache reads are disabled. Key: $key") + } + .as(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[Unit] - 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] = { + ): 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") - } - mode.M.pure(()) - } + }.void } - 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[Unit] = { val key = toKey(keyParts: _*) val finiteTtl = ttl.filter(_.isFinite) // discard Duration.Inf, Duration.Undefined checkFlagsAndPut(key, value, finiteTtl) @@ -63,109 +67,77 @@ 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[Unit] - final override def remove[F[_]](keyParts: Any*)(implicit mode: Mode[F]): F[Any] = + final override def remove(keyParts: Any*): F[Unit] = doRemove(toKey(keyParts: _*)) // REMOVE ALL - protected def doRemoveAll[F[_]]()(implicit mode: Mode[F]): F[Any] + protected def doRemoveAll: F[Unit] - final override def removeAll[F[_]]()(implicit mode: Mode[F]): F[Any] = - doRemoveAll() + final override def removeAll: F[Unit] = + 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._ + ): F[V] = _cachingF(key, ttl, Sync[F].delay(f)) - 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) - } - } - - 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 => - if (logger.isWarnEnabled) { - logger.warn(s"Failed to read from cache. Key = $key", e) - } - None + checkFlagsAndGet(key) + .handleErrorWith { e => + logger + .ifWarnEnabled(logger.warn(s"Failed to read from cache. Key = $key", e)) + .as(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 => + logger.ifWarnEnabled { + 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..735cf1e4 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])( - f: => F[V] - )(implicit mode: Mode[F], flags: Flags): F[V] + def cachingForMemoizeF(baseKey: String)(ttl: Option[Duration])( + 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 90f9f055..8a29845e 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 monad in which all cache operations will be performed. * @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[Unit] /** * 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[Unit] /** * 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[Unit] /** * 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[Unit] } diff --git a/modules/core/shared/src/main/scala/scalacache/Entry.scala b/modules/core/shared/src/main/scala/scalacache/Entry.scala index 7fecb14a..2f845eda 100644 --- a/modules/core/shared/src/main/scala/scalacache/Entry.scala +++ b/modules/core/shared/src/main/scala/scalacache/Entry.scala @@ -1,16 +1,31 @@ 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.Applicative /** * A cache entry with an optional expiry time */ -case class Entry[+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): Boolean = - expiresAt.exists(_.isBefore(Instant.now(clock))) + 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 + } } 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/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..263a7684 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..a44e07cf 100644 --- a/modules/core/shared/src/main/scala/scalacache/package.scala +++ b/modules/core/shared/src/main/scala/scalacache/package.scala @@ -3,8 +3,6 @@ import scala.language.higherKinds package object scalacache { - type Id[X] = X - /** * Get the value corresponding to the given key from the cache. * @@ -18,7 +16,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 +37,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[Unit] = cache.put(keyParts: _*)(value, ttl) /** @@ -56,11 +54,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[V], mode: Mode[F]): F[Any] = + def remove[F[_], V](keyParts: Any*)(implicit cache: Cache[F, V]): F[Unit] = cache.remove(keyParts: _*) 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[Unit] = cache.removeAll } /** @@ -92,7 +90,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 +115,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/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] diff --git a/modules/docs/src/main/mdoc/docs/cache-implementations.md b/modules/docs/src/main/mdoc/docs/cache-implementations.md index d3fffd9f..965093dd 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.unsafeRunSync() } ``` diff --git a/modules/docs/src/main/mdoc/docs/flags.md b/modules/docs/src/main/mdoc/docs/flags.md index 26581e64..c9d5aab5 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.unsafeRunSync() } ``` diff --git a/modules/docs/src/main/mdoc/docs/index.md b/modules/docs/src/main/mdoc/docs/index.md index 392a6c26..c97a1a0d 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.unsafeRunSync() } ``` diff --git a/modules/docs/src/main/mdoc/docs/memoization.md b/modules/docs/src/main/mdoc/docs/memoization.md index 2b884882..6f40dc28 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.unsafeRunSync() } ``` 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) -} -``` 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/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/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..d98bddd9 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,33 @@ 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 { - 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 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" - ) - } - jedisCluster.setex(keyBytes, 1, valueBytes) - case Some(d) => - jedisCluster.setex(keyBytes, d.toSeconds.toInt, valueBytes) - } + override protected def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] = { + 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)) } } - 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 +62,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/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()) 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)) - - } - -} 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) } }