Skip to content

Commit

Permalink
Merge pull request #565 from ronnnnnnnnnnnnn/feature/355-support-poly…
Browse files Browse the repository at this point in the history
…morphic-keys

Support polymorphic keys
  • Loading branch information
lewisjkl authored Nov 9, 2021
2 parents 30a9bab + adfa4dd commit cf0036f
Show file tree
Hide file tree
Showing 54 changed files with 435 additions and 490 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ class CaffeineBenchmark {

implicit val clockSyncIO: Clock[SyncIO] = Clock[SyncIO]

val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]()
implicit val cache: Cache[SyncIO, String] = CaffeineCache[SyncIO, String](underlyingCache)
val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]()
implicit val cache: Cache[SyncIO, String, String] =
CaffeineCache[SyncIO, String, String](underlyingCache)

val key = "key"
val value: String = "value"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ object ProfilingMemoize extends App {

implicit val clockSyncIO = Clock[SyncIO]
val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]()
implicit val cache = CaffeineCache[SyncIO, String](underlyingCache)
implicit val cache = CaffeineCache[SyncIO, String, String](underlyingCache)

val key = "key"
val value: String = "value"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,28 @@
package scalacache.caffeine

import java.time.temporal.ChronoUnit
import java.time.{Instant}

import cats.effect.{Clock, Sync}
import cats.implicits._
import com.github.benmanes.caffeine.cache.{Caffeine, Cache => CCache}
import cats.effect.Clock
import scalacache.logging.Logger
import scalacache.{AbstractCache, CacheConfig, Entry}
import scalacache.{AbstractCache, Entry}

import java.time.Instant
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[F[_]: Sync, V](val underlying: CCache[String, Entry[V]])(implicit
val config: CacheConfig,
clock: Clock[F]
) extends AbstractCache[F, V] {
class CaffeineCache[F[_]: Sync, K, V](val underlying: CCache[K, Entry[V]])(implicit
val clock: Clock[F]
) extends AbstractCache[F, K, V] {
protected val F: Sync[F] = Sync[F]

override protected final val logger = Logger.getLogger(getClass.getName)

def doGet(key: String): F[Option[V]] = {
def doGet(key: K): F[Option[V]] = {
F.delay {
Option(underlying.getIfPresent(key))
}.flatMap(_.filterA(Entry.isBeforeExpiration[F, V]))
Expand All @@ -37,15 +32,15 @@ class CaffeineCache[F[_]: Sync, V](val underlying: CCache[String, Entry[V]])(imp
}
}

def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] =
def doPut(key: K, value: V, ttl: Option[Duration]): F[Unit] =
ttl.traverse(toExpiryTime).flatMap { expiry =>
F.delay {
val entry = Entry(value, expiry)
underlying.put(key, entry)
} *> logCachePut(key, ttl)
}

override def doRemove(key: String): F[Unit] =
override def doRemove(key: K): F[Unit] =
F.delay(underlying.invalidate(key))

override def doRemoveAll: F[Unit] =
Expand All @@ -65,17 +60,17 @@ object CaffeineCache {

/** Create a new Caffeine cache.
*/
def apply[F[_]: Sync: Clock, V](implicit config: CacheConfig): F[CaffeineCache[F, V]] =
Sync[F].delay(Caffeine.newBuilder().build[String, Entry[V]]()).map(apply(_))
def apply[F[_]: Sync: Clock, K <: AnyRef, V]: F[CaffeineCache[F, K, V]] =
Sync[F].delay(Caffeine.newBuilder.build[K, Entry[V]]()).map(apply(_))

/** Create a new cache utilizing the given underlying Caffeine cache.
*
* @param underlying
* a Caffeine cache
*/
def apply[F[_]: Sync: Clock, V](
underlying: CCache[String, Entry[V]]
)(implicit config: CacheConfig): CaffeineCache[F, V] =
def apply[F[_]: Sync: Clock, K, V](
underlying: CCache[K, Entry[V]]
): CaffeineCache[F, K, V] =
new CaffeineCache(underlying)

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,18 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
unsafeRun(f(ticker)) shouldBe Outcome.succeeded(Some(succeed))
}

private def newCCache = Caffeine.newBuilder.build[String, Entry[String]]
case class MyInt(int: Int)

private def newCCache = Caffeine.newBuilder.build[MyInt, Entry[String]]

private def newFCache[F[_]: Sync, V](
underlying: com.github.benmanes.caffeine.cache.Cache[String, Entry[V]]
underlying: com.github.benmanes.caffeine.cache.Cache[MyInt, Entry[V]]
) = {
CaffeineCache[F, V](underlying)
CaffeineCache[F, MyInt, V](underlying)
}

private def newIOCache[V](
underlying: com.github.benmanes.caffeine.cache.Cache[String, Entry[V]]
underlying: com.github.benmanes.caffeine.cache.Cache[MyInt, Entry[V]]
) = {
newFCache[IO, V](underlying)
}
Expand All @@ -49,14 +51,14 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
it should "return the value stored in the underlying cache if expiration is not specified" in ticked { _ =>
val underlying = newCCache
val entry = Entry("hello", expiresAt = None)
underlying.put("key1", entry)
underlying.put(MyInt(1), entry)

newIOCache(underlying).get("key1").map(_ shouldBe Some("hello"))
newIOCache(underlying).get(MyInt(1)).map(_ shouldBe Some("hello"))
}

it should "return None if the given key does not exist in the underlying cache" in ticked { _ =>
val underlying = newCCache
newIOCache(underlying).get("non-existent key").map(_ shouldBe None)
newIOCache(underlying).get(MyInt(2)).map(_ shouldBe None)
}

it should "return None if the given key exists but the value has expired" in ticked { ticker =>
Expand All @@ -65,8 +67,8 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
val underlying = newCCache
val expiredEntry =
Entry("hello", expiresAt = Some(Instant.ofEpochMilli(now.toMillis).minusSeconds(60)))
underlying.put("key1", expiredEntry)
newIOCache(underlying).get("key1").map(_ shouldBe None)
underlying.put(MyInt(1), expiredEntry)
newIOCache(underlying).get(MyInt(1)).map(_ shouldBe None)
}
}

Expand All @@ -76,17 +78,17 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
val underlying = newCCache
val expiredEntry =
Entry("hello", expiresAt = Some(Instant.ofEpochMilli(now.toMillis).plusSeconds(60)))
underlying.put("key1", expiredEntry)
newIOCache(underlying).get("key1").map(_ shouldBe Some("hello"))
underlying.put(MyInt(1), expiredEntry)
newIOCache(underlying).get(MyInt(1)).map(_ shouldBe Some("hello"))
}
}

behavior of "put"

it should "store the given key-value pair in the underlying cache with no TTL" in ticked { _ =>
val underlying = newCCache
newIOCache(underlying).put("key1")("hello", None) *>
IO { underlying.getIfPresent("key1") }
newIOCache(underlying).put(MyInt(1))("hello", None) *>
IO { underlying.getIfPresent(MyInt(1)) }
.map(_ shouldBe Entry("hello", None))
}

Expand All @@ -98,8 +100,8 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi

val underlying = newCCache

newFCache[IO, String](underlying).put("key1")("hello", Some(10.seconds)).map { _ =>
underlying.getIfPresent("key1") should be(Entry("hello", expiresAt = Some(now.plusSeconds(10))))
newFCache[IO, String](underlying).put(MyInt(1))("hello", Some(10.seconds)).map { _ =>
underlying.getIfPresent(MyInt(1)) should be(Entry("hello", expiresAt = Some(now.plusSeconds(10))))
}
}

Expand All @@ -108,8 +110,8 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
val now = Instant.ofEpochMilli(ctx.now().toMillis)

val underlying = newCCache
newFCache[IO, String](underlying).put("key1")("hello", Some(30.days)).map { _ =>
underlying.getIfPresent("key1") should be(
newFCache[IO, String](underlying).put(MyInt(1))("hello", Some(30.days)).map { _ =>
underlying.getIfPresent(MyInt(1)) should be(
Entry("hello", expiresAt = Some(now.plusMillis(30.days.toMillis)))
)
}
Expand All @@ -120,20 +122,20 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
it should "delete the given key and its value from the underlying cache" in ticked { _ =>
val underlying = newCCache
val entry = Entry("hello", expiresAt = None)
underlying.put("key1", entry)
underlying.getIfPresent("key1") should be(entry)
underlying.put(MyInt(1), entry)
underlying.getIfPresent(MyInt(1)) should be(entry)

newIOCache(underlying).remove("key1") *>
IO(underlying.getIfPresent("key1")).map(_ shouldBe null)
newIOCache(underlying).remove(MyInt(1)) *>
IO(underlying.getIfPresent(MyInt(1))).map(_ shouldBe null)
}

behavior of "get after put"

it should "store the given key-value pair in the underlying cache with no TTL, then get it back" in ticked { _ =>
val underlying = newCCache
val cache = newIOCache(underlying)
cache.put("key1")("hello", None) *>
cache.get("key1").map { _ shouldBe defined }
cache.put(MyInt(1))("hello", None) *>
cache.get(MyInt(1)).map { _ shouldBe defined }
}

behavior of "get after put with TTL"
Expand All @@ -143,28 +145,28 @@ class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter wi
val underlying = newCCache
val cache = newFCache[IO, String](underlying)

cache.put("key1")("hello", Some(5.seconds)) *>
cache.get("key1").map { _ shouldBe defined }
cache.put(MyInt(1))("hello", Some(5.seconds)) *>
cache.get(MyInt(1)).map { _ shouldBe defined }
}

it should "store the given key-value pair with the given TTL, then get it back (after a sleep) when not expired" in ticked {
implicit ticker =>
val underlying = newCCache
val cache = newFCache[IO, String](underlying)

cache.put("key1")("hello", Some(50.seconds)) *>
cache.put(MyInt(1))("hello", Some(50.seconds)) *>
IO.sleep(40.seconds) *> // sleep, but not long enough for the entry to expire
cache.get("key1").map { _ shouldBe defined }
cache.get(MyInt(1)).map { _ shouldBe defined }
}

it should "store the given key-value pair with the given TTL, then return None if the entry has expired" in ticked {
implicit ticker =>
val underlying = newCCache
val cache = newFCache[IO, String](underlying)

cache.put("key1")("hello", Some(50.seconds)) *>
cache.put(MyInt(1))("hello", Some(50.seconds)) *>
IO.sleep(60.seconds) *> // sleep long enough for the entry to expire
cache.get("key1").map { _ shouldBe empty }
cache.get(MyInt(1)).map { _ shouldBe empty }
}

}
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package scalacache.serialization

import java.nio.ByteBuffer

import io.circe.jawn.JawnParser
import io.circe.{Decoder, Encoder}
import scalacache.serialization.binary.BinaryCodec

package object circe {

private[this] val parser = new JawnParser

implicit def codec[A](implicit encoder: Encoder[A], decoder: Decoder[A]): Codec[A] = new Codec[A] {
implicit def codec[A](implicit encoder: io.circe.Encoder[A], decoder: io.circe.Decoder[A]): BinaryCodec[A] =
new BinaryCodec[A] {

override def encode(value: A): Array[Byte] = encoder.apply(value).noSpaces.getBytes("utf-8")
override def encode(value: A): Array[Byte] = encoder.apply(value).noSpaces.getBytes("utf-8")

override def decode(bytes: Array[Byte]): Codec.DecodingResult[A] =
parser.decodeByteBuffer(ByteBuffer.wrap(bytes)).left.map(FailedToDecode)
override def decode(bytes: Array[Byte]): Codec.DecodingResult[A] =
parser.decodeByteBuffer(ByteBuffer.wrap(bytes)).left.map(FailedToDecode)

}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.circe.syntax._
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
import scalacache.serialization.binary.BinaryCodec

case class Fruit(name: String, tastinessQuotient: Double)

Expand All @@ -15,7 +16,7 @@ class CirceCodecSpec extends AnyFlatSpec with Matchers with ScalaCheckDrivenProp

import scalacache.serialization.circe._

private def serdesCheck[A: Arbitrary](expectedJson: A => String)(implicit codec: Codec[A]): Unit = {
private def serdesCheck[A: Arbitrary](expectedJson: A => String)(implicit codec: BinaryCodec[A]): Unit = {
forAll(minSuccessful(10000)) { (a: A) =>
val serialised = codec.encode(a)
new String(serialised, "utf-8") shouldBe expectedJson(a)
Expand Down Expand Up @@ -62,7 +63,7 @@ class CirceCodecSpec extends AnyFlatSpec with Matchers with ScalaCheckDrivenProp

it should "serialize and deserialize a case class" in {
import io.circe.generic.auto._
val fruitCodec = implicitly[Codec[Fruit]]
val fruitCodec = implicitly[BinaryCodec[Fruit]]

val banana = Fruit("banana", 0.7)
val serialised = fruitCodec.encode(banana)
Expand Down
23 changes: 12 additions & 11 deletions modules/core/src/main/scala-2/scalacache/memoization/Macros.scala
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
package scalacache.memoization

import scala.language.experimental.macros
import scala.reflect.macros.blackbox
import scalacache.{Cache, Flags}

import scala.concurrent.duration.Duration
import scala.language.experimental.macros
import scala.language.higherKinds
import scalacache.{Flags, Cache}
import scala.reflect.macros.blackbox

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[F, V]], flags: c.Expr[Flags]): c.Tree = {
)(f: c.Tree)(cache: c.Expr[Cache[F, String, V]], config: c.Expr[MemoizationConfig], flags: c.Expr[Flags]): c.Tree = {
commonMacroImpl(
cache,
config,
{ keyName =>
q"""$cache.cachingForMemoize($keyName)($ttl)($f)($flags)"""
q"""$cache.caching($keyName)($ttl)($f)($flags)"""
}
)
}

def memoizeFImpl[F[_], V: c.WeakTypeTag](
ttl: c.Expr[Option[Duration]]
)(f: c.Tree)(cache: c.Expr[Cache[F, V]], flags: c.Expr[Flags]): c.Tree = {
)(f: c.Tree)(cache: c.Expr[Cache[F, String, V]], config: c.Expr[MemoizationConfig], flags: c.Expr[Flags]): c.Tree = {
commonMacroImpl(
cache,
config,
{ keyName =>
q"""$cache.cachingForMemoizeF($keyName)($ttl)($f)($flags)"""
q"""$cache.cachingF($keyName)($ttl)($f)($flags)"""
}
)
}

private def commonMacroImpl[F[_], V: c.WeakTypeTag](
cache: c.Expr[Cache[F, V]],
config: c.Expr[MemoizationConfig],
keyNameToCachingCall: (c.TermName) => c.Tree
): Tree = {

Expand All @@ -52,7 +53,7 @@ class Macros(val c: blackbox.Context) {
val keyName = createKeyName()
val cachingCall = keyNameToCachingCall(keyName)
val tree = q"""
val $keyName = $cache.config.memoization.toStringConverter.toString($classNameTree, $classParamssTree, $methodNameTree, $methodParamssTree)
val $keyName = $config.toStringConverter.toString($classNameTree, $classParamssTree, $methodNameTree, $methodParamssTree)
$cachingCall
"""
// println(showCode(tree))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ package object memoization {
* @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[F, V], flags: Flags): F[V] =
def memoize[F[_], V](ttl: Option[Duration])(
f: => V
)(implicit cache: Cache[F, String, V], config: MemoizationConfig, flags: Flags): F[V] =
macro Macros.memoizeImpl[F, V]

/** Perform the given operation and memoize its result to a cache before returning it. If the result is already in the
Expand Down Expand Up @@ -64,6 +66,6 @@ package object memoization {
*/
def memoizeF[F[_], V](
ttl: Option[Duration]
)(f: F[V])(implicit cache: Cache[F, V], flags: Flags): F[V] =
)(f: F[V])(implicit cache: Cache[F, String, V], config: MemoizationConfig, flags: Flags): F[V] =
macro Macros.memoizeFImpl[F, V]
}
Loading

0 comments on commit cf0036f

Please sign in to comment.