Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/jcouyang/jujiu
Functional Scala Cache
https://github.com/jcouyang/jujiu
birds cache caffeine functional-programming scala
Last synced: 1 day ago
JSON representation
Functional Scala Cache
- Host: GitHub
- URL: https://github.com/jcouyang/jujiu
- Owner: jcouyang
- License: apache-2.0
- Created: 2019-05-26T07:36:30.000Z (over 5 years ago)
- Default Branch: master
- Last Pushed: 2024-12-11T17:14:13.000Z (16 days ago)
- Last Synced: 2024-12-18T15:07:05.632Z (9 days ago)
- Topics: birds, cache, caffeine, functional-programming, scala
- Language: Scala
- Homepage:
- Size: 524 KB
- Stars: 58
- Watchers: 3
- Forks: 6
- Open Issues: 9
-
Metadata Files:
- Readme: README.org
- License: LICENSE
Awesome Lists containing this project
README
#+HTML:
雎鳩ju jiu
Functional Scala Caching
[[https://github.com/jcouyang/jujiu/actions][https://github.com/jcouyang/jujiu/workflows/Build%20and%20Test/badge.svg]]
[[https://index.scala-lang.org/jcouyang/jujiu][https://index.scala-lang.org/jcouyang/jujiu/latest.svg?v=1]]
[[https://www.javadoc.io/doc/us.oyanglul/jujiu_0.23][https://www.javadoc.io/badge/us.oyanglul/jujiu_2.13.svg?label=document]]
[[https://codecov.io/gh/jcouyang/jujiu][https://codecov.io/gh/jcouyang/jujiu/branch/master/graph/badge.svg]]#+HTML:
#+BEGIN_QUOTE
*Do one thing and do it well* micro [[https://github.com/search?q=org:jcouyang+topic:birds&type=Repositories][birds library]] series
#+END_QUOTE~val version =~ [[https://index.scala-lang.org/jcouyang/jujiu][https://index.scala-lang.org/jcouyang/jujiu/latest.svg?v=1]]
#+BEGIN_EXAMPLE
libraryDependencies += "us.oyanglul" %% "jujiu" % version
#+END_EXAMPLE* Quick Started in Scala 3^{=new=}
:PROPERTIES:
:header-args: :tangle src/test/scala-3.0.0-M3/us/oyanglul/JujiuSpec.scala :exports code
:CUSTOM_ID: scala-3-example
:END:
#+begin_src scala :exports none
package us.oyanglul.jujiuimport us.oyanglul.jujiu.syntax.caffeine._
import us.oyanglul.jujiu.syntax.cache._
import scala.concurrent.ExecutionContext
import org.specs2.mutable.Specification
import cats.effect._
import com.github.benmanes.caffeine.cache
#+end_srcThere are only two simple steps to use cache:
1. Initiate a protocol-agnostic Cache DSL, which means the DSL only aware of what to operate, not how to
2. using the DSL syntax =fetchF=, =parFetchAllF= etc to describe how to use the Cache in the program
3. =given= a instance of =Cache[Key, Val]=, in the example we created a Caffeine instance, which tells *how* exactly how to actually do the cache.#+begin_src scala
class JujiuScala3Spec extends Specification:
given ContextShift[IO] = IO.contextShift(ExecutionContext.global)
"works with IO" >> {
"normal cache" >> {
val dsl: Cache[IO, cache.Cache, String, String] = new CaffeineCache[IO, String, String]{}def program(using cache.Cache[String, String]) =
for
_ <- IO(println("something"))
_ <- dsl.putF("key1", "value1")
r1 <- dsl.fetchF("key1")
r2 <- dsl.fetchF("key2", _ => IO("value2"))
r3 <- dsl.fetchAllF(List("key1", "key2"))
r4 <- dsl.parFetchAllF[List, IO.Par](List("key1", "key2"))
_ <- dsl.clearF("key1")
yield (r1, r2, r3, r4)given cache.Cache[String, String] = Caffeine().sync[String, String]
program.unsafeRunSync() must_== (
(
Some("value1"),
"value2",
List(Some("value1"), Some("value2")),
List(Some("value1"), Some("value2"))
)
)
}
}
end JujiuScala3Spec
#+end_src* [[https://typelevel.org/cats/img/cats-badge-tiny.png]] [[https://github.com/ben-manes/caffeine][Caffeine]]
:PROPERTIES:
:header-args: :tangle no :exports code
:CUSTOM_ID: making-caffeine-cats-friendly-badge
:END:
#+BEGIN_SRC scala :exports none :noweb yes :tangle src/test/scala/us/oyanglul/JujiuSpec.scala
package us.oyanglul.jujiu
import us.oyanglul.jujiu.syntax.caffeine._
import us.oyanglul.jujiu.syntax.cache._
import cats.{Applicative}
import cats.data.Kleisli
import java.util.concurrent.CompletableFuture
import scala.concurrent.ExecutionContext
import org.specs2.mutable.Specification
import cats.instances.list._
import cats.syntax.all._
import cats.effect._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import com.github.benmanes.caffeine.cache
import us.oyanglul.jujiu.syntax.CaffeineSyntaxclass JujiuSpec extends Specification with org.specs2.mock.Mockito {
implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
<>
<>
<>
<>
<>
<>
<>
<>
}
#+END_SRC#+BEGIN_SRC scala -n :noweb-ref get_set_cache
"it should able to get and set cache" >> {
object cacheDsl extends CaffeineCache[IO, String, String] // <- (ref:dsl)
val program = for {
r1 <- cacheDsl.fetch("not exist yet") // <- (ref:fetch)
r2 <- cacheDsl.fetch("not exist yet", _ => IO("default")) // <- (ref:fetchOr)
_ <- cacheDsl.put("not exist yet", "now exist") // <- (ref:put)
r3 <- cacheDsl.fetch("not exist yet")
_ <- cacheDsl.clear("not exist yet")
r4 <- cacheDsl.fetch("not exist yet")
} yield (r1, r2, r3, r4)
program(Caffeine().sync) // <- (ref:run)
.unsafeRunSync() must_== ((None, "default", Some("now exist"), None))
}
#+END_SRC#+BEGIN_SRC scala :exports none :noweb-ref async_load_failure
"it should IO error when async load failure" >> {
object dsl extends CaffeineAsyncCache[IO, String, String] {
implicit val executionContext = global
}
val program = for {
r1 <- dsl.fetch("not exist yet")
r2 <- dsl.fetch("not exist yet", _ => IO("default"))
} yield (r1, r2)
val failCache = mock[cache.AsyncCache[String, String]]
failCache.getIfPresent("not exist yet") returns CompletableFuture.supplyAsync(() => IO.raiseError[String](new Exception("cache load error")).unsafeRunSync())
program(
failCache
).unsafeRunSync() must throwA[Exception](message = "cache load error")
}
#+END_SRC#+BEGIN_QUOTE
This README is a *literal programming* file, all code here will generate the [[https://github.com/jcouyang/jujiu/blob/master/src/test/scala/us/oyanglul/JujiuSpec.scala][test]] file
#+END_QUOTEI can walk you through line by line though
- [[(dsl)][line-(dsl)]] creates an instance of =CaffeineCache= which has side effect =IO=,
key is =String= and value is =String= as well
- [[(fetch)][line-(fetch)]] won't acutally trigger any effect, it just returns a
DSL, represent as type =Klesili[IO, Cache, String]= which in English,
"give me a =Cache= and I can provide you an
=IO[String]="- [[(fetchOr)][line-(fetchOr)]] is new =fetch= DSL, the second parameter is a function
=K => IO[V]=, if cache not exist, it will run the function can put the
result into the cache, and return the value- [[(put)][line-(put)]] will update the value of key "not exist yet" to "overrided"
- [[(run)][line-(run)]] is the Scala idiomatic syntax to build synchronize
Caffeine Cache\\
if you still recall that the =program= is actually
=Klesili[IO, Cache, String]= so now\\
I provide it a =Cache= by =program(Caffeine().sync)=\\
it shall return me a =IO[String]= =.unsafeRunSync()= the IO and all
effects you described before in =program= will be triggered\\
and you will get the actual result** works with Cats IO
:PROPERTIES:
:CUSTOM_ID: and-cats-io
:END:
Jujiu has very flexible DSL, If you don't like Kleisli, it works with IO(technically you IO type just need to be a =Async=) as wellwhat you'll need to import some syntax
#+BEGIN_SRC scala :exports none :tangle no
import us.oyanglul.jujiu.syntax.cache._
#+END_SRC#+BEGIN_SRC scala :exports none :noweb-ref catsio_cache
"works with IO" >> {
"normal cache" >> {
val c: Cache[IO, cache.Cache, String, String] = new CaffeineCache[IO, String, String] {}
implicit val cacheProvider: cache.Cache[String, String] = Caffeine().sync[String, String]
def program =
for {
_ <- IO(println("something"))
_ <- c.putF("key1", "value1")
r1 <- c.fetchF("key1")
r2 <- c.fetchF("key2", _ => IO("value2"))
r3 <- c.fetchAllF(List("key1", "key2"))
r4 <- c.parFetchAllF[List, IO.Par](List("key1", "key2"))
_ <- c.clearF("key1")
} yield (r1, r2, r3, r4)
program.unsafeRunSync() must_== (
(
Some("value1"),
"value2",
List(Some("value1"), Some("value2")),
List(Some("value1"), Some("value2"))
)
)
}#+END_SRC
and provide =cacheProvider= implicitly, since you are not using Kleisli, you need to tell what cache
these DSLs will run on#+BEGIN_SRC scala :noweb-ref catsio_loading_cache
"loading cache" >> {
val c: LoadingCache[IO, cache.LoadingCache, String, String] = new CaffeineLoadingCache[IO, String, String] {}
implicit val cacheProvider: cache.LoadingCache[String, String] = Caffeine().sync(identity)
def program =
for {
_ <- IO(println("something"))
r1 <- c.fetchF("1")
r2 <- c.fetchAllF(List("2", "3"))
r3 <- c.parFetchAllF[List, IO.Par](List("4", "5"))
} yield (r1, r2, r3)
program.unsafeRunSync() must_== (("1", List("2", "3"), List("4", "5")))
}
}
#+END_SRC#+BEGIN_QUOTE
similar to =ExecutionContext=, you need to provide context the thread can run on
#+END_QUOTEand all dsl suffix with =F=
** idiomatic syntax for Caffeine builderDealing with Java DSL and Java Future is too verbose and painful in
Scala projectLet's see how Jiujiu makes Caffeine friendly to Cats IO as well
A good example is the Async Loading Cache
First you will need caffeine builder syntax
#+BEGIN_SRC scala :export none :tangle no
import us.oyanglul.jujiu.syntax.caffeine._
#+END_SRC#+BEGIN_SRC scala :noweb-ref caffeine_builder
"it should able to get and set async loading cache" >> {
object cache extends CaffeineAsyncLoadingCache[IO, Integer, String] {
implicit val executionContext = global // <-- (ref:executionContext)
}val program = for {
r1 <- cache.fetch(1)
r2 <- cache.fetch(2)
r3 <- cache.fetchAll(List[Integer](1, 2, 3))
} yield (r1, r2, r3)val caffeineA: com.github.benmanes.caffeine.cache.AsyncLoadingCache[Integer, String] = Caffeine()
.executionContext(global) // <-- (ref:global)
.withExpire( // <-- (ref:expire)
(_: Integer, _: String) => 1.second,
(_: Integer, _: String, currentDuration: FiniteDuration) => currentDuration,
(_: Integer, _: String, currentDuration: FiniteDuration) => currentDuration
)
.async((key: Integer) => IO("async string" + key)) // <-- (ref:async)val caffeineB = Caffeine()
.withExpireAfterAccess(1.second)
.withExpireAfterWrite(2.seconds)
.withRefreshAfterWrite(3.seconds)
.async((key: Integer) => IO("async string" + key))val expected = (
"async string1",
"async string2",
List("async string1", "async string2", "async string3")
)
program(caffeineA).unsafeRunSync() must_== expected
program(caffeineB).unsafeRunSync() must_== expected
program(Caffeine().async(_ => IO.raiseError(new Exception("something wrong"))))
.unsafeRunSync() must throwA[Exception]
}
#+END_SRC- [[(executionContext)][line-(executionContext)]] Async Loading Cache need an Execution Context to execute the Java
Future things- [[(global)][line-(global)]] =.executionContext(global)= will make sure the cache using Scala
execution context as default to execute java future, otherwise its default java folk join pool.
alternatively you can also use Akka's execution context.- [[(expire)][line-(expire)]] default the expiring policy, here it's more Scala idiomatic
lambda and =Duration=- [[(async)][line-(async)]] will create an
async loading cache.
the async loading function that it will use is =K => IO[V]= so you
don't need to deal with awful Java Future.** Works with Tagless Final
No matter what style of effect abstraction you project is using, Jujiu can easily fit ini.e. Tagless Final
#+BEGIN_SRC scala :noweb-ref tagless_final
"works with tagless final" >> {
trait LogDsl[F[_]] {
def log(msg: String): F[Unit]
}type ProgramDsl[F[_]] = CaffeineCache[F, String, String] with LogDsl[F]
def program[F[_]: Async](dsl: ProgramDsl[F])
(implicit ev: cache.Cache[String, String]): F[Option[String]] =
for {
value <- dsl.fetchF("key")
_ <- dsl.log("something")
} yield value{
object dsl extends CaffeineCache[IO, String, String] with LogDsl[IO] {
def log(msg: String) = IO(println(msg))
}implicit val cacheProvider: cache.Cache[String, String] = Caffeine().sync[String, String]
program[IO](dsl).unsafeRunSync() must_== None
}
}
#+END_SRCjust =extends CaffeineCache[F, K, V]= and provide =cacheProvider=
** ReaderT Pattern
if your code is in ReaderT pattern, good, it will fit in more naturally
#+BEGIN_SRC scala :noweb-ref readerT
"works with tagless final style readerT" >> {
// Layer 1: Environment
trait HasLogger {
def logger: String => Unit
}
trait HasCacheProvider {
def cacheProvider: cache.Cache[String, String]
}type Env = HasLogger with HasCacheProvider
// Layer 2: DSL
trait LogDsl[F[_]] {
def log(msg: String)(implicit M: Applicative[F]): Kleisli[F, Env, Unit] = Kleisli(a => M.pure(a.logger(msg)))
}type Dsl[F[_]] = CaffeineCache[F, String, String] with LogDsl[F]
// Layer 3: Business
def program[F[_]](dsl: Dsl[F])(
implicit ev: Async[F]
) =
for {
_ <- dsl.log("something")
value <- dsl.fetch("key").local[Env](_.cacheProvider)
} yield valueobject dsl extends CaffeineCache[IO, String, String] with LogDsl[IO]
program[IO](dsl)
.run(new HasLogger with HasCacheProvider {
def logger = println
def cacheProvider = Caffeine().sync
})
.unsafeRunSync() must_== None
}
#+END_SRCnotice that proper contravariant adapt need =.local[Env](_.cacheProvider)=
** Extensible
it's extensible by design as Kleisli, if you provider another cache provider, the same dsl
will work.
#+BEGIN_SRC scala :noweb-ref redis
"run on redis" >> {
import redis.clients.jedis._def program[F[_]: Async, S[_, _]](dsl: Cache[F, S, String, String]) = for {
r1 <- dsl.fetch("not exist yet")
r2 <- dsl.fetch("not exist yet", _ => Async[F].delay("default"))
_ <- dsl.put("not exist yet", "now exist")
r3 <- dsl.fetch("not exist yet")
_ <- dsl.clear("not exist yet")
r4 <- dsl.fetch("not exist yet")
} yield (r1, r2, r3, r4)type J[A, B] = Jedis
object dsl extends Cache[IO, J, String, String] {
def put(k: String, v: String)(implicit M: Async[IO]): Kleisli[IO, Jedis, Unit] =
Kleisli { redis =>
M.delay{
redis.set(k, v)
()
}
}
def fetch(k: String)(implicit M: Async[IO]): Kleisli[IO, Jedis, Option[String]] =
Kleisli(redis => M.delay(Option(redis.get(k))))
def clear(k: String)(implicit M: Async[IO]): Kleisli[IO, Jedis, Unit] =
Kleisli(redis => M.delay{
redis.del(k)
()
})
}program(dsl).run(
new Jedis("localhost")
).unsafeRunSync() must_== ((None, "default", Some("now exist"), None))
}.pendingUntilFixed("Redis")
#+END_SRC