Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend Cache interface with Option-specific use case #725

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 37 additions & 20 deletions modules/core/src/main/scala/scalacache/AbstractCache.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,26 +96,43 @@ trait AbstractCache[F[_], K, V] extends Cache[F, K, V] with LoggingSupport[F, K]
key: K
)(ttl: Option[Duration] = None)(f: => V)(implicit flags: Flags): F[V] = cachingF(key)(ttl)(Sync[F].delay(f))

final override def cachingOption(
key: K
)(ttl: Option[Duration] = None)(f: => Option[V])(implicit flags: Flags): F[Option[V]] =
cachingFOption(key)(ttl)(Sync[F].delay(f))

override def cachingF(
key: K
)(ttl: Option[Duration] = None)(f: F[V])(implicit flags: Flags): F[V] = {
checkFlagsAndGet(key)
.handleErrorWith { e =>
logger
.ifWarnEnabled(logger.warn(s"Failed to read from cache. Key = $key", e))
.as(None)
}
.flatMap {
case Some(valueFromCache) => F.pure(valueFromCache)
case None =>
f.flatTap { calculatedValue =>
checkFlagsAndPut(key, calculatedValue, ttl)
.handleErrorWith { e =>
logger.ifWarnEnabled {
logger.warn(s"Failed to write to cache. Key = $key", e)
}.void
}
}
}
}
)(ttl: Option[Duration] = None)(f: F[V])(implicit flags: Flags): F[V] =
read(key).flatMap {
case Some(valueFromCache) => F.pure(valueFromCache)
case None => f.flatTap(write(key, _, ttl))
}

override def cachingFOption(
key: K
)(ttl: Option[Duration] = None)(f: F[Option[V]])(implicit flags: Flags): F[Option[V]] =
read(key).flatMap {
case Some(valueFromCache) => F.pure(Some(valueFromCache))
case None =>
f.flatTap {
case Some(calculatedValue) => write(key, calculatedValue, ttl)
case None => logger.ifDebugEnabled(logger.debug("Calculated value was empty, not writing into cache")).void
}
}

private def read(key: K)(implicit flags: Flags): F[Option[V]] =
checkFlagsAndGet(key).handleErrorWith { e =>
logger
.ifWarnEnabled(logger.warn(s"Failed to read from cache. Key = $key", e))
.as(None)
}

private def write(key: K, value: V, ttl: Option[Duration])(implicit flags: Flags): F[Unit] =
checkFlagsAndPut(key, value, ttl).handleErrorWith { e =>
logger.ifWarnEnabled {
logger.warn(s"Failed to write to cache. Key = $key", e)
}.void
}

}
32 changes: 32 additions & 0 deletions modules/core/src/main/scala/scalacache/Cache.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ trait Cache[F[_], K, V] {
*/
def caching(key: K)(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 (only when it isn't None),
* and return it.
*
* @param key
* 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 (optional) value
* @param flags
* Flags used to conditionally alter the behaviour of ScalaCache
* @return
* The value, either retrieved from the cache or computed
*/
def cachingOption(key: K)(ttl: Option[Duration])(f: => Option[V])(implicit flags: Flags): F[Option[V]]

/** Get a value from the cache if it exists. Otherwise compute it, insert it into the cache, and return it.
*
* @param key
Expand All @@ -95,6 +111,22 @@ trait Cache[F[_], K, V] {
*/
def cachingF(key: K)(ttl: Option[Duration])(f: F[V])(implicit flags: Flags): F[V]

/** Get a value from the cache if it exists. Otherwise compute it, insert it into the cache (only when it isn't None),
* and return it.
*
* @param key
* 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 (optional) value
* @param flags
* Flags used to conditionally alter the behaviour of ScalaCache
* @return
* The value, either retrieved from the cache or computed
*/
def cachingFOption(key: K)(ttl: Option[Duration])(f: F[Option[V]])(implicit flags: Flags): F[Option[V]]

/** You should call this when you have finished using this Cache. (e.g. when your application shuts down)
*
* It will take care of gracefully shutting down the underlying cache client.
Expand Down
64 changes: 64 additions & 0 deletions modules/core/src/test/scala/scalacache/AbstractCacheSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,36 @@ class AbstractCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter {
result should be("value from cache")
}

it should "run the Option block and cache resulting Some value" in {
var called = false
val result = cache
.cachingOption("myKey")(None) {
called = true
Some("result of block")
}
.unsafeRunSync()

cache.getCalledWithArgs(0) should be("myKey")
cache.putCalledWithArgs(0) should be(("myKey", "result of block", None))
called should be(true)
result should be(Some("result of block"))
}

it should "run the Option block and not cache resulting None value" in {
var called = false
val result = cache
.cachingOption("myKey")(None) {
called = true
None
}
.unsafeRunSync()

cache.getCalledWithArgs(0) should be("myKey")
cache.putCalledWithArgs should be(empty)
called should be(true)
result should be(None)
}

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 {
Expand Down Expand Up @@ -170,6 +200,40 @@ class AbstractCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter {
tResult should be("value from cache")
}

it should "run the Option block and cache resulting Some value" in {
var called = false
val result = cache
.cachingFOption("myKey")(None) {
SyncIO {
called = true
Some("result of block")
}
}
.unsafeRunSync()

cache.getCalledWithArgs(0) should be("myKey")
cache.putCalledWithArgs(0) should be(("myKey", "result of block", None))
called should be(true)
result should be(Some("result of block"))
}

it should "run the Option block and not cache resulting None value" in {
var called = false
val result = cache
.cachingFOption("myKey")(None) {
SyncIO {
called = true
None
}
}
.unsafeRunSync()

cache.getCalledWithArgs(0) should be("myKey")
cache.putCalledWithArgs should be(empty)
called should be(true)
result should be(None)
}

behavior of "#caching (sync mode)"

it should "run the block and cache its result if the value is not found in the cache" in {
Expand Down