Shared Resource Cache for Cats Effect
Micro-library that provides a way to memoize Resource[F, A]
so they would be shared between accesses.
The resource is allocated once when the first consumer uses it and is deallocated when the last consumer stops using it.
Motivation
The main use-case for developing this was sharing resources like database connections between test suites.
For example:
import cats.effect.*
import cats.effect.resource_shared_memoized.*
import cats.effect.syntax.all.*
import cats.effect.unsafe.implicits.global
// A fake database connection
class DatabaseConnection extends AutoCloseable {
override def close() = {}
}
object MySuite {
val database = ResourceSharedMemoized.memoize(
Resource.fromAutoCloseable(IO(new DatabaseConnection))
).unsafeRunSync()
}
class MyTests extends munit.CatsEffectSuite {
val dbFixture = ResourceFunFixture(MySuite.database)
dbFixture.test("test 1") { db =>
// The `db` object here is shared with...
val _ = db // pretend that we use db
}
dbFixture.test("test 2") { db =>
// The `db` object here.
val _ = db // pretend that we use db
}
}
This allows you to run one test case, a suite of tests, or all tests and not have to worry about acquiring or releasing the database connection.
Can't we just use .memoize
?
Unfortunately no. memoize
returns Resource[F, Resource[F, A]]
, and we need Resource[F, A]
. If we use
.allocated
on the outer resource then we need to perform the cleanup ourselves, which ResourceSharedMemoized
handles for you.
Take a look at this example:
case class State(acquires: Int, releases: Int, users: Int) {
def acquire = copy(acquires = acquires + 1, users = users + 1)
def release = copy(releases = releases + 1, users = users - 1)
override def toString: String = s"[acquires=$acquires, releases=$releases, users=$users]"
}
val (memoizedResource, memoizedResourceState) = (for {
state <- Ref[IO].of(State(0, 0, 0))
resource = Resource.make(state.modify { s =>
val newState = s.acquire
(newState, newState)
})(_ => state.update(_.release))
resource <- resource.memoize.allocated.map(_._1)
} yield (resource, state)).unsafeRunSync()
// memoizedResource: Resource[IO, State] = Eval(
// fa = FlatMap(
// ioe = Delay(
// thunk = cats.effect.kernel.SyncRef$$Lambda$11998/0x0000000103308040@6e43352e,
// event = cats.effect.tracing.TracingEvent$StackTrace
// ),
// f = cats.effect.kernel.GenConcurrent$$Lambda$11999/0x0000000103308840@2a387979,
// event = cats.effect.tracing.TracingEvent$StackTrace
// )
// )
// memoizedResourceState: Ref[IO, State] = cats.effect.kernel.SyncRef@7e8f3e28
memoizedResource.use { value =>
memoizedResourceState.get.map(state => s"\nstate: $state\nvalue: $value")
}.unsafeRunSync()
// res0: String = """
// state: [acquires=1, releases=0, users=1]
// value: [acquires=1, releases=0, users=1]"""
// The resource is NOT deallocated when the last consumer stops using it.
memoizedResourceState.get.unsafeRunSync()
// res1: State = State(acquires = 1, releases = 0, users = 1)
memoizedResource.use { value1 =>
memoizedResourceState.get.flatMap { state1 =>
memoizedResource.use { value2 =>
memoizedResourceState.get.map { state2 =>
// All values should be the same
s"\nstate1: $state1\nvalue1: $value1\nstate2: $state2\nvalue2: $value2"
}
}
}
}.unsafeRunSync()
// res2: String = """
// state1: [acquires=1, releases=0, users=1]
// value1: [acquires=1, releases=0, users=1]
// state2: [acquires=1, releases=0, users=1]
// value2: [acquires=1, releases=0, users=1]"""
// The resource is NOT deallocated when the last consumer stops using it.
memoizedResourceState.get.unsafeRunSync()
// res3: State = State(acquires = 1, releases = 0, users = 1)
We want to use the Resource[F, A]
transparently, without caring about managing lifetimes for it.
Usage
There is a single function ResourceSharedMemoized.memoize
that takes a Resource[F, A]
and returns Resource[F, A]
.
An .memoizeShared
extension method is also provided for Resource[F, A]
in the
cats.effect.resource_shared_memoized.ResourceSharedMemoizedOps
implicit class.
The example below demonstrates on how the library works:
import cats.effect.resource_shared_memoized.*
val (cachedResource, state) = (for {
state <- Ref[IO].of(State(0, 0, 0))
resource = Resource.make(state.modify { s =>
val newState = s.acquire
(newState, newState)
})(_ => state.update(_.release))
resource <- resource.memoizeShared
} yield (resource, state)).unsafeRunSync()
// cachedResource: Resource[[A]IO[A], State] = Allocate(
// resource = cats.effect.kernel.Resource$$$Lambda$11991/0x0000000103303040@11b6efbe
// )
// state: Ref[IO, State] = cats.effect.kernel.SyncRef@715f24c4
cachedResource.use { value =>
state.get.map(state => s"\nstate: $state\nvalue: $value")
}.unsafeRunSync()
// res4: String = """
// state: [acquires=1, releases=0, users=1]
// value: [acquires=1, releases=0, users=1]"""
// The resource is deallocated when the last consumer stops using it.
state.get.unsafeRunSync()
// res5: State = State(acquires = 1, releases = 1, users = 0)
cachedResource.use { value1 =>
state.get.flatMap { state1 =>
cachedResource.use { value2 =>
state.get.map { state2 =>
// All values should be the same
s"\nstate1: $state1\nvalue1: $value1\nstate2: $state2\nvalue2: $value2"
}
}
}
}.unsafeRunSync()
// res6: String = """
// state1: [acquires=2, releases=1, users=1]
// value1: [acquires=2, releases=1, users=1]
// state2: [acquires=2, releases=1, users=1]
// value2: [acquires=2, releases=1, users=1]"""
// The resource is deallocated when the last consumer stops using it.
state.get.unsafeRunSync()
// res7: State = State(acquires = 2, releases = 2, users = 0)
Installation
Add the following to your build.sbt
:
libraryDependencies +=
"io.github.arturaz" %% "cats-effect-resource-shared-memoized" % "0.1.1"
Or build.sc
if you are using mill:
override def ivyDeps = Agg(
ivy"io.github.arturaz::cats-effect-resource-shared-memoized:0.1.1"
)
The code from main
branch can be obtained with:
resolvers ++= Resolver.sonatypeOssRepos("snapshots")
libraryDependencies +=
"io.github.arturaz" %% "cats-effect-resource-shared-memoized" % "0.1.1-1-59c5fd9-SNAPSHOT"
For mill:
override def repositoriesTask = T.task {
super.repositoriesTask() ++ Seq(
coursier.Repositories.sonatype("snapshots")
)
}
override def ivyDeps = Agg(
ivy"io.github.arturaz::cats-effect-resource-shared-memoized:0.1.1-1-59c5fd9-SNAPSHOT"
)
Credits
Artūras Šlajus (https://github.com/arturaz)