Skip to content

Commit

Permalink
Merge e914ecd into 62f066b
Browse files Browse the repository at this point in the history
  • Loading branch information
blemale committed Mar 3, 2019
2 parents 62f066b + e914ecd commit d1633d7
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 105 deletions.
2 changes: 1 addition & 1 deletion project/Dependencies.scala
@@ -1,7 +1,7 @@
import sbt._

object Dependencies {
val CaffeineVersion = "2.6.2"
val CaffeineVersion = "2.7.0"

val Caffeine = "com.github.ben-manes.caffeine" % "caffeine" % CaffeineVersion
val Java8Compat = "org.scala-lang.modules" %% "scala-java8-compat" % "0.9.0"
Expand Down
80 changes: 80 additions & 0 deletions src/main/scala/com/github/blemale/scaffeine/AsyncCache.scala
@@ -0,0 +1,80 @@
package com.github.blemale.scaffeine

import java.util.concurrent.Executor

import com.github.benmanes.caffeine.cache.{ AsyncCache => CaffeineAsyncCache }

import scala.compat.java8.FunctionConverters._
import scala.compat.java8.FutureConverters._
import scala.concurrent.Future

object AsyncCache {
def apply[K, V](asyncCache: CaffeineAsyncCache[K, V]): AsyncCache[K, V] =
new AsyncCache(asyncCache)
}

class AsyncCache[K, V](val underlying: CaffeineAsyncCache[K, V]) {

/**
* Returns the future associated with `key` in this cache, or `None` if there is no
* cached future for `key`.
*
* @param key key whose associated value is to be returned
* @return an option containing the current (existing or computed) future value to which the
* specified key is mapped, or `None` if this map contains no mapping for the key
*/
def getIfPresent(key: K): Option[Future[V]] =
Option(underlying.getIfPresent(key)).map(_.toScala)

/**
* Returns the future associated with `key` in this cache, obtaining that value from
* `mappingFunction` if necessary. This method provides a simple substitute for the
* conventional "if cached, return; otherwise create, cache and return" pattern.
*
* @param key key with which the specified value is to be associated
* @param mappingFunction the function to asynchronously compute a value
* @return the current (existing or computed) future value associated with the specified key
*/
def get(key: K, mappingFunction: K => V): Future[V] =
underlying.get(key, asJavaFunction(mappingFunction)).toScala

/**
* Returns the future associated with `key` in this cache, obtaining that value from
* `mappingFunction` if necessary. This method provides a simple substitute for the
* conventional "if cached, return; otherwise create, cache and return" pattern.
*
* @param key key with which the specified value is to be associated
* @param mappingFunction the function to asynchronously compute a value
* @return the current (existing or computed) future value associated with the specified key
* @throws java.lang.RuntimeException or Error if the mappingFunction does when constructing the future,
* in which case the mapping is left unestablished
*/
def getFuture(key: K, mappingFunction: K => Future[V]): Future[V] =
underlying.get(
key,
asJavaBiFunction((k: K, _: Executor) => mappingFunction(k).toJava.toCompletableFuture)
).toScala

/**
* Associates `value` with `key` in this cache. If the cache previously contained a
* value associated with `key`, the old value is replaced by `value`. If the
* asynchronous computation fails, the entry will be automatically removed.
*
* @param key key with which the specified value is to be associated
* @param valueFuture value to be associated with the specified key
*/
def put(key: K, valueFuture: Future[V]): Unit =
underlying.put(key, valueFuture.toJava.toCompletableFuture)

/**
* Returns a view of the entries stored in this cache as a synchronous [[Cache]]. A
* mapping is not present if the value is currently being loaded. Modifications made to the
* synchronous cache directly affect the asynchronous cache. If a modification is made to a
* mapping that is currently loading, the operation blocks until the computation completes.
*
* @return a thread-safe synchronous view of this cache
*/
def synchronous(): Cache[K, V] =
Cache(underlying.synchronous())

}
@@ -1,11 +1,8 @@
package com.github.blemale.scaffeine

import java.util.concurrent.Executor

import com.github.benmanes.caffeine.cache.{ AsyncLoadingCache => CaffeineAsyncLoadingCache }

import scala.collection.JavaConverters._
import scala.compat.java8.FunctionConverters._
import scala.compat.java8.FutureConverters._
import scala.concurrent.Future

Expand All @@ -14,49 +11,9 @@ object AsyncLoadingCache {
new AsyncLoadingCache(asyncLoadingCache)
}

class AsyncLoadingCache[K, V](val underlying: CaffeineAsyncLoadingCache[K, V]) {
class AsyncLoadingCache[K, V](override val underlying: CaffeineAsyncLoadingCache[K, V]) extends AsyncCache[K, V](underlying) {
private[this] implicit val ec = DirectExecutionContext

/**
* Returns the future associated with `key` in this cache, or `None` if there is no
* cached future for `key`.
*
* @param key key whose associated value is to be returned
* @return an option containing the current (existing or computed) future value to which the
* specified key is mapped, or `None` if this map contains no mapping for the key
*/
def getIfPresent(key: K): Option[Future[V]] =
Option(underlying.getIfPresent(key)).map(_.toScala)

/**
* Returns the future associated with `key` in this cache, obtaining that value from
* `mappingFunction` if necessary. This method provides a simple substitute for the
* conventional "if cached, return; otherwise create, cache and return" pattern.
*
* @param key key with which the specified value is to be associated
* @param mappingFunction the function to asynchronously compute a value
* @return the current (existing or computed) future value associated with the specified key
*/
def get(key: K, mappingFunction: K => V): Future[V] =
underlying.get(key, asJavaFunction(mappingFunction)).toScala

/**
* Returns the future associated with `key` in this cache, obtaining that value from
* `mappingFunction` if necessary. This method provides a simple substitute for the
* conventional "if cached, return; otherwise create, cache and return" pattern.
*
* @param key key with which the specified value is to be associated
* @param mappingFunction the function to asynchronously compute a value
* @return the current (existing or computed) future value associated with the specified key
* @throws java.lang.RuntimeException or Error if the mappingFunction does when constructing the future,
* in which case the mapping is left unestablished
*/
def getFuture(key: K, mappingFunction: K => Future[V]): Future[V] =
underlying.get(
key,
asJavaBiFunction((k: K, _: Executor) => mappingFunction(k).toJava.toCompletableFuture)
).toScala

/**
* Returns the future associated with `key` in this cache, obtaining that value from
* `loader` if necessary. If the asynchronous computation fails, the entry
Expand All @@ -83,17 +40,6 @@ class AsyncLoadingCache[K, V](val underlying: CaffeineAsyncLoadingCache[K, V]) {
def getAll(keys: Iterable[K]): Future[Map[K, V]] =
underlying.getAll(keys.asJava).toScala.map(_.asScala.toMap)

/**
* Associates `value` with `key` in this cache. If the cache previously contained a
* value associated with `key`, the old value is replaced by `value`. If the
* asynchronous computation fails, the entry will be automatically removed.
*
* @param key key with which the specified value is to be associated
* @param valueFuture value to be associated with the specified key
*/
def put(key: K, valueFuture: Future[V]): Unit =
underlying.put(key, valueFuture.toJava.toCompletableFuture)

/**
* Returns a view of the entries stored in this cache as a synchronous [[LoadingCache]]. A
* mapping is not present if the value is currently being loaded. Modifications made to the
Expand All @@ -102,7 +48,7 @@ class AsyncLoadingCache[K, V](val underlying: CaffeineAsyncLoadingCache[K, V]) {
*
* @return a thread-safe synchronous view of this cache
*/
def synchronous(): LoadingCache[K, V] =
override def synchronous(): LoadingCache[K, V] =
LoadingCache(underlying.synchronous())

override def toString = s"AsyncLoadingCache($underlying)"
Expand Down
14 changes: 14 additions & 0 deletions src/main/scala/com/github/blemale/scaffeine/Scaffeine.scala
Expand Up @@ -306,6 +306,20 @@ case class Scaffeine[K, V](underlying: caffeine.cache.Caffeine[K, V]) {
)
))

/**
* Builds a cache which does not automatically load values when keys are requested unless a
* mapping function is provided. The returned [[scala.concurrent.Future]] may be already loaded or
* currently computing the value for a given key. If the asynchronous computation fails
* value then the entry will be automatically removed. Note that multiple
* threads can concurrently load values for distinct keys.
*
* @tparam K1 the key type of the cache
* @tparam V1 the value type of the cache
* @return a cache having the requested features
*/
def buildAsync[K1 <: K, V1 <: V](): AsyncCache[K1, V1] =
AsyncCache(underlying.buildAsync[K1, V1]())

/**
* Builds a cache, which either returns a [[scala.concurrent.Future]] already loaded or currently
* computing the value for a given key, or atomically computes the value asynchronously through a
Expand Down
65 changes: 65 additions & 0 deletions src/test/scala/com/github/blemale/scaffeine/AsyncCacheSpec.scala
@@ -0,0 +1,65 @@
package com.github.blemale.scaffeine

import org.scalatest.concurrent.ScalaFutures
import org.scalatest.{ Matchers, OptionValues, WordSpec }

import scala.concurrent.Future

class AsyncCacheSpec
extends WordSpec
with Matchers
with ScalaFutures
with OptionValues {

"AsyncCache" should {
"get value if present" in {
val cache = Scaffeine().buildAsync[String, String]()

cache.put("foo", Future.successful("present"))
val fooValue = cache.getIfPresent("foo")
val barValue = cache.getIfPresent("bar")

fooValue.value.futureValue should be("present")
barValue should be(None)
}

"get or compute value" in {
val cache = Scaffeine().buildAsync[String, String]()

cache.put("foo", Future.successful("present"))
val fooValue = cache.get("foo", k => "computed")
val barValue = cache.get("bar", k => "computed")

fooValue.futureValue should be("present")
barValue.futureValue should be("computed")
}

"get or compute async value" in {
val cache = Scaffeine().buildAsync[String, String]()

cache.put("foo", Future.successful("present"))
val fooValue = cache.getFuture("foo", k => Future.successful("computed"))
val barValue = cache.getFuture("bar", k => Future.successful("computed"))

fooValue.futureValue should be("present")
barValue.futureValue should be("computed")
}

"put value" in {
val cache = Scaffeine().buildAsync[String, String]()

cache.put("foo", Future.successful("present"))
val fooValue = cache.getIfPresent("foo")

fooValue.value.futureValue should be("present")
}

"expose a synchronous view of itself" in {
val cache = Scaffeine().buildAsync[String, String]()

val synchronousCache = cache.synchronous()

synchronousCache shouldBe a[Cache[_, _]]
}
}
}
Expand Up @@ -13,20 +13,8 @@ class AsyncLoadingCacheSpec

"AsyncLoadingCache" when {
"created with synchronous loader" should {
"get value if present" in {
import scala.concurrent.ExecutionContext.Implicits.global
val cache = Scaffeine().buildAsync[String, String]((key: String) => "loaded")

cache.put("foo", Future.successful("present"))
val fooValue = cache.getIfPresent("foo")
val barValue = cache.getIfPresent("bar")

fooValue.value.futureValue should be("present")
barValue should be(None)
}

"get or load value" in {
import scala.concurrent.ExecutionContext.Implicits.global
val cache = Scaffeine().buildAsync[String, String]((key: String) => "loaded")

cache.put("foo", Future.successful("present"))
Expand All @@ -37,32 +25,7 @@ class AsyncLoadingCacheSpec
barValue.futureValue should be("loaded")
}

"get or compute value" in {
import scala.concurrent.ExecutionContext.Implicits.global
val cache = Scaffeine().buildAsync[String, String]((key: String) => "loaded")

cache.put("foo", Future.successful("present"))
val fooValue = cache.get("foo", k => "computed")
val barValue = cache.get("bar", k => "computed")

fooValue.futureValue should be("present")
barValue.futureValue should be("computed")
}

"get or compute async value" in {
import scala.concurrent.ExecutionContext.Implicits.global
val cache = Scaffeine().buildAsync[String, String]((key: String) => "loaded")

cache.put("foo", Future.successful("present"))
val fooValue = cache.getFuture("foo", k => Future.successful("computed"))
val barValue = cache.getFuture("bar", k => Future.successful("computed"))

fooValue.futureValue should be("present")
barValue.futureValue should be("computed")
}

"get or load all given values" in {
import scala.concurrent.ExecutionContext.Implicits.global
val cache = Scaffeine().buildAsync[String, String]((key: String) => "loaded")

cache.put("foo", Future.successful("present"))
Expand All @@ -72,7 +35,6 @@ class AsyncLoadingCacheSpec
}

"get or bulk load all given values" in {
import scala.concurrent.ExecutionContext.Implicits.global
val cache = Scaffeine().buildAsync[String, String](
(key: String) => "loaded",
allLoader = Some((keys: Iterable[String]) => keys.map(_ -> "bulked").toMap)
Expand All @@ -84,16 +46,6 @@ class AsyncLoadingCacheSpec
values.futureValue should contain only ("foo" -> "present", "bar" -> "bulked")
}

"put value" in {
import scala.concurrent.ExecutionContext.Implicits.global
val cache = Scaffeine().buildAsync[String, String]((key: String) => "loaded")

cache.put("foo", Future.successful("present"))
val fooValue = cache.getIfPresent("foo")

fooValue.value.futureValue should be("present")
}

"expose a synchronous view of itself" in {
val cache = Scaffeine().buildAsync[String, String]((key: String) => "loaded")

Expand Down

0 comments on commit d1633d7

Please sign in to comment.