Skip to content

Commit 0c7486c

Browse files
Merge pull request #99 from ChristopherDavenport/caffeineIntegration
Include Caffeine Integration
2 parents d15ed7c + 3eba300 commit 0c7486c

File tree

7 files changed

+161
-14
lines changed

7 files changed

+161
-14
lines changed

build.sbt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,31 @@ lazy val mules = project.in(file("."))
22
.disablePlugins(MimaPlugin)
33
.settings(skip in publish := true)
44
.settings(commonSettings)
5-
.aggregate(core, reload, noop)
5+
.aggregate(core, caffeine, reload, noop, bench)
66

77
lazy val bench = project.in(file("modules/bench"))
88
.disablePlugins(MimaPlugin)
99
.enablePlugins(JmhPlugin)
1010
.settings(skip in publish := true)
1111
.settings(commonSettings)
12-
.dependsOn(core)
12+
.dependsOn(core, caffeine)
1313

1414
lazy val core = project.in(file("modules/core"))
1515
.settings(commonSettings)
1616
.settings(
1717
name := "mules"
1818
)
1919

20+
lazy val caffeine = project.in(file("modules/caffeine"))
21+
.settings(commonSettings)
22+
.dependsOn(core)
23+
.settings(
24+
name := "mules-caffeine",
25+
libraryDependencies ++= Seq(
26+
"com.github.ben-manes.caffeine" % "caffeine" % "2.8.0"
27+
)
28+
)
29+
2030
lazy val noop = project.in(file("modules/noop"))
2131
.settings(commonSettings)
2232
.dependsOn(core)

modules/bench/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,21 @@
44
sbt
55
project bench
66
jmh:run -i 3 -wi 3 -f1 -t15 // iterations 3, warmup iterations 3, forks 1, threads 15
7+
```
8+
9+
10+
## Numbers presently
11+
12+
13+
```
14+
[info] Benchmark Mode Cnt Score Error Units
15+
[info] LookUpBench.contentionCaffeine thrpt 10 68602.693 ± 183.322 ops/s
16+
[info] LookUpBench.contentionConcurrentHashMap thrpt 10 26815.305 ± 47.615 ops/s
17+
[info] LookUpBench.contentionSingleImmutableMap thrpt 10 21853.931 ± 82.138 ops/s
18+
[info] LookUpBench.contentionReadsCaffeine thrpt 10 88898.190 ± 676.454 ops/s
19+
[info] LookUpBench.contentionReadsConcurrentHashMap thrpt 10 28990.070 ± 161.409 ops/s
20+
[info] LookUpBench.contentionReadsSingleImmutableMap thrpt 10 24290.804 ± 233.290 ops/s
21+
[info] LookUpBench.contentionWritesCaffeine thrpt 10 74592.814 ± 811.518 ops/s
22+
[info] LookUpBench.contentionWritesConcurrentHashMap thrpt 10 40196.853 ± 247.774 ops/s
23+
[info] LookUpBench.contentionWritesSingleImmutableMap thrpt 10 28423.209 ± 215.411 ops/s
724
```

modules/bench/src/main/scala/io/chrisdavenport/mules/LookupBench.scala

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package io.chrisdavenport.mules
22

3-
import java.util.concurrent.TimeUnit
3+
// import java.util.concurrent.TimeUnit
44
import org.openjdk.jmh.annotations._
55

66
import cats.implicits._
77
import cats.effect._
8+
import io.chrisdavenport.mules.caffeine.CaffeineCache
89

910

10-
@BenchmarkMode(Array(Mode.AverageTime))
11-
@OutputTimeUnit(TimeUnit.MILLISECONDS)
11+
@BenchmarkMode(Array(Mode.Throughput))
12+
// @OutputTimeUnit(TimeUnit.MILLISECONDS)
1213
class LookUpBench {
1314
import LookUpBench._
1415

@@ -20,7 +21,11 @@ class LookUpBench {
2021
def contentionConcurrentHashMap(in: BenchStateCHM) =
2122
testUnderContention(in.memoryCache, in.readList, in.writeList)(in.CS)
2223

23-
def testUnderContention(m: MemoryCache[IO, Int, String], r: List[Int], w: List[Int])(implicit CS: ContextShift[IO]) = {
24+
@Benchmark
25+
def contentionCaffeine(in: BenchStateCaffeine) =
26+
testUnderContention(in.cache, in.readList, in.writeList)(in.CS)
27+
28+
def testUnderContention(m: Cache[IO, Int, String], r: List[Int], w: List[Int])(implicit CS: ContextShift[IO]) = {
2429
val set = w.traverse( m.insert(_, "foo"))
2530
val read = r.traverse(m.lookup(_))
2631
val action = (set, read).parMapN((_, _) => ())
@@ -35,7 +40,11 @@ class LookUpBench {
3540
def contentionReadsConcurrentHashMap(in: BenchStateCHM) =
3641
underContentionWaitReads(in.memoryCache, in.readList, in.writeList)(in.CS)
3742

38-
def underContentionWaitReads(m: MemoryCache[IO, Int, String], r: List[Int], w: List[Int])(implicit CS: ContextShift[IO]) = {
43+
@Benchmark
44+
def contentionReadsCaffeine(in: BenchStateCaffeine) =
45+
underContentionWaitReads(in.cache, in.readList, in.writeList)(in.CS)
46+
47+
def underContentionWaitReads(m: Cache[IO, Int, String], r: List[Int], w: List[Int])(implicit CS: ContextShift[IO]) = {
3948
val set = w.traverse(m.insert(_, "foo"))
4049
val read = r.traverse(m.lookup(_))
4150
Concurrent[IO].bracket(set.start)(
@@ -51,7 +60,11 @@ class LookUpBench {
5160
def contentionWritesConcurrentHashMap(in: BenchStateCHM) =
5261
underContentionWaitWrites(in.memoryCache, in.readList, in.writeList)(in.CS)
5362

54-
def underContentionWaitWrites(m: MemoryCache[IO, Int, String],r: List[Int], w: List[Int])(implicit CS: ContextShift[IO]) = {
63+
@Benchmark
64+
def contentionWritesCaffeine(in: BenchStateCaffeine) =
65+
underContentionWaitWrites(in.cache, in.readList, in.writeList)(in.CS)
66+
67+
def underContentionWaitWrites(m: Cache[IO, Int, String],r: List[Int], w: List[Int])(implicit CS: ContextShift[IO]) = {
5568
val set = w.traverse( m.insert(_, "foo"))
5669
val read = r.traverse(m.lookup(_))
5770
Concurrent[IO].bracket(read.start)(
@@ -91,4 +104,19 @@ object LookUpBench {
91104
}
92105

93106
}
107+
108+
@State(Scope.Benchmark)
109+
class BenchStateCaffeine {
110+
var cache: Cache[IO, Int, String] = _
111+
val writeList: List[Int] = (1 to 100).toList
112+
val readList : List[Int] = (1 to 100).toList
113+
implicit val T = IO.timer(scala.concurrent.ExecutionContext.global)
114+
implicit val CS = IO.contextShift(scala.concurrent.ExecutionContext.global)
115+
116+
@Setup(Level.Trial)
117+
def setup(): Unit = {
118+
cache = CaffeineCache.build[IO, Int, String](None, None, None).unsafeRunSync()
119+
cache.insert(1, "yellow").unsafeRunSync()
120+
}
121+
}
94122
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package io.chrisdavenport.mules.caffeine
2+
3+
4+
import cats.implicits._
5+
import io.chrisdavenport.mules.Cache
6+
import com.github.benmanes.caffeine.cache.{Caffeine, Cache => CCache}
7+
import cats.effect._
8+
import io.chrisdavenport.mules.TimeSpec
9+
import java.util.concurrent.TimeUnit
10+
11+
private class CaffeineCache[F[_], K, V](cc: CCache[K, V])(implicit sync: Sync[F]) extends Cache[F, K, V]{
12+
// Members declared in io.chrisdavenport.mules.Delete
13+
def delete(k: K): F[Unit] = sync.delay(cc.invalidate(k))
14+
15+
// Members declared in io.chrisdavenport.mules.Insert
16+
def insert(k: K, v: V): F[Unit] = sync.delay(cc.put(k, v))
17+
18+
// Members declared in io.chrisdavenport.mules.Lookup
19+
def lookup(k: K): F[Option[V]] =
20+
sync.delay(Option(cc.getIfPresent(k)))
21+
22+
}
23+
24+
25+
object CaffeineCache {
26+
27+
/**
28+
* insertWithTimeout does not operate as the underlying cache is fully responsible for these values
29+
**/
30+
def build[F[_]: Sync, K, V](
31+
defaultTimeout: Option[TimeSpec],
32+
accessTimeout: Option[TimeSpec],
33+
maxSize: Option[Long]
34+
): F[Cache[F, K, V]] = {
35+
Sync[F].delay(Caffeine.newBuilder())
36+
.map(b => defaultTimeout.fold(b)(ts => b.expireAfterWrite(ts.nanos, TimeUnit.NANOSECONDS)))
37+
.map(b => accessTimeout.fold(b)(ts => b.expireAfterAccess(ts.nanos, TimeUnit.NANOSECONDS)))
38+
.map(b => maxSize.fold(b)(b.maximumSize))
39+
.map(_.build[K with Object, V with Object]())
40+
.map(_.asInstanceOf[CCache[K, V]]) // 2.12 hack
41+
.map(fromCache[F, K, V](_))
42+
}
43+
44+
/** Build a Cache from a Caffeine Cache **/
45+
def fromCache[F[_]: Sync, K, V](cache: CCache[K, V]): Cache[F, K, V] =
46+
new CaffeineCache[F, K, V](cache)
47+
48+
49+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package io.chrisdavenport.mules.caffeine
2+
3+
import org.specs2.mutable.Specification
4+
import scala.concurrent.duration._
5+
import cats.effect._
6+
// import cats.effect.implicits._
7+
import cats.effect.IO
8+
import cats.effect.specs2.CatsIO
9+
import io.chrisdavenport.mules.TimeSpec
10+
11+
class CaffeineCacheSpec extends Specification with CatsIO {
12+
"CaffeineCache" should {
13+
"get a value in a quicker period than the timeout" in {
14+
val setup = for {
15+
cache <- CaffeineCache.build[IO, String, Int](Some(TimeSpec.unsafeFromDuration(1.second)), None, None)
16+
_ <- cache.insert("Foo", 1)
17+
_ <- Timer[IO].sleep(1.milli)
18+
value <- cache.lookup("Foo")
19+
} yield value
20+
setup.map(_ must_=== Some(1))
21+
}
22+
23+
24+
"remove a value after delete" in {
25+
val setup = for {
26+
cache <- CaffeineCache.build[IO, String, Int](None, None, None)
27+
_ <- cache.insert("Foo", 1)
28+
_ <- cache.delete("Foo")
29+
value <- cache.lookup("Foo")
30+
} yield value
31+
setup.map(_ must_=== None)
32+
}
33+
34+
35+
"Lookup after interval fails to get a value" in {
36+
val setup = for {
37+
cache <- CaffeineCache.build[IO, String, Int](Some(TimeSpec.unsafeFromDuration(1.second)), None, None)
38+
_ <- cache.insert("Foo", 1)
39+
_ <- Timer[IO].sleep(2.second)
40+
value <- cache.lookup("Foo")
41+
} yield value
42+
setup.map(_ must_=== None)
43+
}
44+
45+
46+
}
47+
}

modules/core/src/main/scala/io/chrisdavenport/mules/Cache.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ trait Get[F[_], K, V]{
1010

1111
trait Insert[F[_], K, V]{
1212
def insert(k: K, v: V): F[Unit]
13-
def insertWithTimeout(optionTimeout: Option[TimeSpec])(k: K, v: V): F[Unit]
1413
}
1514

1615
trait Delete[F[_], K]{
@@ -20,6 +19,4 @@ trait Delete[F[_], K]{
2019
trait Cache[F[_], K, V]
2120
extends Lookup[F, K, V]
2221
with Insert[F, K, V]
23-
with Delete[F, K]
24-
25-
trait GetCache[F[_], K, V] extends Get[F, K, V] with Cache[F, K, V]
22+
with Delete[F, K]

modules/noop/src/main/scala/io/chrisdavenport/mules/noop/NoOpCache.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.chrisdavenport.mules.noop
22

3-
import io.chrisdavenport.mules.{Cache, TimeSpec}
3+
import io.chrisdavenport.mules.Cache
44
import cats.Applicative
55

66
private class NoOpCache[F[_], K, V](implicit F: Applicative[F]) extends Cache[F, K, V]{
@@ -10,7 +10,6 @@ private class NoOpCache[F[_], K, V](implicit F: Applicative[F]) extends Cache[F,
1010

1111
// Members declared in io.chrisdavenport.mules.Insert
1212
def insert(k: K, v: V): F[Unit] = F.unit
13-
def insertWithTimeout(optionTimeout: Option[TimeSpec])(k: K, v: V): F[Unit] = F.unit
1413

1514
// Members declared in io.chrisdavenport.mules.Lookup
1615
def lookup(k: K): F[Option[V]] = noneF

0 commit comments

Comments
 (0)