Case study: The digital transformation of Santa’s logistical nightmare - Part 2
In this blog post, we’d like to go through how Santa’s workshop makes use of cats-effect (CE3) to bootstrap itself.
You can check out the rest of the series:
- Part 1: Introduction
- Part 2: This post
- Part 3: F[F[S]] Kafka! fs2-kafka
- Part 4: http4s
Before we get started, we want to say that the Typelevel documentation on cats-effect is very complete and well written - so we strongly recommend reading through those if you haven’t had any exposure to cats-effect.
With that out of the way, let’s get our paws wet and see how we can leverage CE3 to wrangle together all these side effects in our Main
class.
One of the first things you’ll see is that this isn’t a regular scala App
but instead extends IOApp.Simple
.
This provides us with an entry point to our application which will evaluate an IO[Unit]
that we give it.
It is also capable of handling interruption signals by cancelling our application and releasing resources.
If you need access to either program arguments or the ability to return a specific exit code - you’ll need to look at the standard IOApp
variant.
Carrying on to the definition of loadKafkaConfigFromEnv
(see below) of Main.scala
, we have some code that loads the necessary environment variables and if not those values are not there the whole application will fail with a MissingConfig
error. We also want the return type of this definition to be IO[KafkaConfig]
. A more common approach for production apps and configuration values is to use libraries such as Ciris or PureConfig.
To read environment variables we use Env[IO]
which is part of CE3 and it’s there for that very reason, pretty handy. On the second and third lines of the code above, we have the two Option[String]
environment variables that we need to combine to complete our configuration - here you can see us using mapN
to combine the options. We want our app to fail bootstrapping if the configuration is missing, so we use liftTo
to convert Option.None
into an IO.Error(MissingConfig)
.
Coming back to the definition of run
, the last thing we want to do is initialise our application’s resources and run it forever until it receives an interrupt.
We do this by passing our configuration to SantaServer.resource
which returns a Resource[IO, Server]
. We allocate our resources and allow the program to run until terminated by calling useForever
on the Resource
(There will be more on using Resource in other posts). One minor thing to note here, is that we call .unit
afterwards - this is because useForever
has a return type of Nothing
to indicate that the code will never return normally and this triggers a dead code warning from the compiler. We could refactor the code or use a no warning annotation here, but we opted for .void
to change the type to IO[Unit]
instead.
As part of the application shutdown, finalizers run for the resources we initialised. This means all resources related to Kafka Consumers, Http4s Server, etc are all cleanly terminated… which is Nice. You’ll likely run into Resource
quite often and it is talked about in the CE3 documentation we mentioned earlier, so if you’d like to learn more we’d recommend checking it out!
Notes
Soon after starting we had to decide what style to write our application in. You’ll frequently come across a pattern called ‘Tagless Final’ where implementations are expressed with an abstract F[_]
effect type and dependencies commonly injected as context bounds.
We chose not to do this and to write our code directly against IO[_]
to make the examples more accessible.
In our example you will see IO as a type parameter to many traits. Some of them defined by libraries (like in the case of Env) and some are defined by us, type parameters work as usual and also they are sign of how we want to deal with all side effects.
Cat Tax!
You made it all the way to the end!! wooo! Here is a picture of us! (the authors of this post!!)
Dog & Bones |
Megachu |