Essential Effects: Happy new year, plus background tasks management
Happy New Year everybody!
I didn’t finish the book in 2020, but, you know, stuff happened. Many folks who took my course or have read a draft of the book wanted something that would highlight the main points of the book, so I’m currently writing a final chapter: a job scheduler case study.
I’ve got some good feedback coming in from early reviewers, and I hope to soon make the next iteration available for early-release purchase.
Here’s some new content: how to use Resource
to control the lifetime of a background task.
Be well,
.. Adam
Example: Managing a background task
A perhaps less obvious use of a Resource
is to manage the lifecycle of a background task. For example, we may want to fork some (often non-terminating) effect, and later cancel it when it isn’t required to run anymore. This suggests we can combine use of a Fiber
with a Resource
, where the Resource
effects are defined as:
- acquire:
start
the task, producing aFiber
- release:
cancel
theFiber
Then the lifetime of the background task would directly correspond to the execution of the use
effect of the Resource
:
package com.innerproduct.ee.resources import cats.effect._ import cats.implicits._ import com.innerproduct.ee.debug._ import scala.concurrent.duration._ object ResourceBackgroundTask extends IOApp { def run(args: List[String]): IO[ExitCode] = for { _ <- backgroundTask.use { _ => IO.sleep(200.millis) *> IO("$s is so cool!").debug // <1> } _ <- IO("done!").debug } yield ExitCode.Success val backgroundTask: Resource[IO, Unit] = { val loop = (IO("looping...").debug *> IO.sleep(100.millis)).foreverM // <2> Resource .make(IO("> forking backgroundTask").debug *> loop.start)( // <3> IO("< canceling backgroundTask").debug.void *> _.cancel // <4> ) .void // <5> } }
<1> The background task will only be running during our use
effect. We sleep a little bit, to ensure the background task does some work.
<2> The background task itself is a loop that prints and sleeps. We use the foreverM
combinator, which is equivalent to
- val loop: IO[Nothing] = step.flatMap(_ => loop) + val loop: IO[Nothing] = step.foreverM
<3> The acquire effect forks a Fiber
and…
<4> … the release effect cancels it.
<5> In this example we don’t give the user of the Resource
access to the Fiber
, although you could imagine where this may be useful.
ResourceBackgroundTask
outputs:
[ioapp-compute-0] > forking backgroundTask <1> [ioapp-compute-1] looping... [ioapp-compute-2] looping... [ioapp-compute-3] looping... [ioapp-compute-4] $s is so cool! <2> [ioapp-compute-4] < canceling backgroundTask <3> [ioapp-compute-4] done!
<1> Our effect is forked as a Fiber
.
<2> Once the use
effect finishes…
<3> … the Fiber
is canceled.
Since this “background task
” pattern is common, Cats Effect defines the background
method on an IO
:
def background: Resource[IO, IO[A]] // <1>
<1> The resource “value” is an IO[A]
, which is an effect which lets you join the effect running in the background; it’s literally the join
method of the Fiber
that the Resource
manages.
Our code from the example–with debug
effects removed–could be rewritten as:
- Resource.make(loop.start)(_.cancel) + loop.background