IOLocal
IOLocal
provides a handy way of manipulating a context on different scopes.
In some scenarios, IOLocal
can be considered as an alternative to cats.data.Kleisli
.
IOLocal
should not be treated as Ref
, since the former abides different laws.
Once a fiber is forked, for example by Spawn[F].start
, the forked fiber manipulates the
copy of the parent's context. For example, two forked fibers will never see each other's
modifications to the same IOLocal
, each fiber will only see its own modifications.
IOLocal
are visible to the fiber
Operations on ┌────────────┐ ┌────────────┐ ┌────────────┐
│ Fiber A │ update(_ + 1) │ Fiber A │ update(_ + 1) │ Fiber A │
│ (local 42) │──────────────►│ (local 43) │──────────────►│ (local 44) │
└────────────┘ └────────────┘ └────────────┘
import cats.effect.{IO, IOLocal}
import scala.concurrent.duration._
def inc(idx: Int, local: IOLocal[Int]): IO[Unit] =
local.update(_ + 1) >> local.get.flatMap(current => IO.println(s"update $idx: $current"))
for {
local <- IOLocal(42)
_ <- inc(1, local)
_ <- inc(2, local)
current <- local.get
_ <- IO.println(s"fiber A: $current")
} yield ()
// output:
// update 1: 43
// update 2: 44
// fiber A: 44
IOLocal
A forked fiber operates on a copy of the parent A forked fiber (i.e. via Spawn[F].start
) operates on a copy of the parent IOLocal
.
Hence, the children operations are not reflected on the parent context.
┌────────────┐ ┌────────────┐
fork │ Fiber B │ update(_ - 1) │ Fiber B │
┌─────►│ (local 42) │──────────────►│ (local 41) │
│ └────────────┘ └────────────┘
┌────────────┐─┘ ┌────────────┐
│ Fiber A │ │ Fiber A │
│ (local 42) │────────────────────────────────────►│ (local 42) │
└────────────┘─┐ └────────────┘
│ ┌────────────┐ ┌────────────┐
│ fork │ Fiber C │ update(_ + 1) │ Fiber C │
└─────►│ (local 42) │──────────────►│ (local 43) │
└────────────┘ └────────────┘
def update(name: String, local: IOLocal[Int], f: Int => Int): IO[Unit] =
local.update(f) >> local.get.flatMap(current => IO.println(s"$name: $current"))
for {
local <- IOLocal(42)
fiberA <- update("fiber B", local, _ - 1).start
fiberB <- update("fiber C", local, _ + 1).start
_ <- fiberA.joinWithNever
_ <- fiberB.joinWithNever
current <- local.get
_ <- IO.println(s"fiber A: $current")
} yield ()
// output:
// fiber B: 41
// fiber C: 43
// fiber A: 42
IOLocal
are invisible to children
Parent operations on ┌────────────┐ ┌────────────┐
fork │ Fiber B │ update(_ + 1) │ Fiber B │
┌─────►│ (local 42) │──────────────►│ (local 43) │
│ └────────────┘ └────────────┘
┌────────────┐─┘ ┌────────────┐
│ Fiber A │ update(_ - 1) │ Fiber A │
│ (local 42) │────────────────────────────────────►│ (local 41) │
└────────────┘─┐ └────────────┘
│ ┌────────────┐ ┌────────────┐
│ fork │ Fiber C │ update(_ + 2) │ Fiber C │
└─────►│ (local 42) │──────────────►│ (local 44) │
└────────────┘ └────────────┘
Example: TraceId propagation
The IOLocal
can be used for the propagation of a TraceId
:
import cats.Monad
import cats.effect.{IO, IOLocal, Sync, Resource}
import cats.effect.std.{Console, Random}
import cats.syntax.flatMap._
import cats.syntax.functor._
case class TraceId(value: String)
object TraceId {
def gen[F[_]: Sync]: F[TraceId] =
Random.scalaUtilRandom[F].flatMap(_.nextString(8)).map(TraceId(_))
}
trait TraceIdScope[F[_]] {
def get: F[TraceId]
def scope(traceId: TraceId): Resource[F, Unit]
}
object TraceIdScope {
def apply[F[_]](implicit ev: TraceIdScope[F]): TraceIdScope[F] = ev
def fromIOLocal: IO[TraceIdScope[IO]] =
for {
local <- IOLocal(TraceId("global"))
} yield new TraceIdScope[IO] {
def get: IO[TraceId] =
local.get
def scope(traceId: TraceId): Resource[IO, Unit] =
Resource.make(local.getAndSet(traceId))(previous => local.set(previous)).void
}
}
def service[F[_]: Sync: Console: TraceIdScope]: F[String] =
for {
traceId <- TraceId.gen[F]
result <- TraceIdScope[F].scope(traceId).use(_ => callRemote[F])
} yield result
def callRemote[F[_]: Monad: Console: TraceIdScope]: F[String] =
for {
traceId <- TraceIdScope[F].get
_ <- Console[F].println(s"Processing request. TraceId: ${traceId}")
} yield "some response"
TraceIdScope.fromIOLocal.flatMap { implicit traceIdScope: TraceIdScope[IO] =>
service[IO]
}