This is an example project for writing backend services by mixing ZIO, Cats-Effect IO and Akka libraries. The template of this is somewhat similar to our Prezi-specific template which currently does not take advantage of ZIO or Cats libraries.
This example implements the following goals:
- Use ZIO on top level to be able to bootstrap the service in a more controlled way
- Use ZIO's environment support for passing around the global dependencies in the whole service
- Be able to use any library with a cats-effect IO interface
- Use Akka-HTTP to implement the HTTP endpoint
- Ability to express concurrent logic with either akka actors or cats or zio effects
- Ability to interop between Akka Streams and ZIO streams
- Make this all testable
- Use Lightbend's config library for configuration, as it is used by all the Akka based libraries
In the README I will highlight some parts of the example but see the source code for the full example.
TODO: rewrite this part with info about layers
The main
function builds up the environment, then creates the service API handler and a test actor and
runs the service.
One of the dependencies is AkkaContext
. This holds the actor system for Akka (note that a materializer is no longer needed as it is tied to the system since Akka 2.6).
This environment is added as a managed resource that ensures that it gets properly terminated:
private val create: ZIO[ServiceSpecificOptions, Nothing, AkkaContext] =
for {
opts <- options
} yield new AkkaContext {
override val actorSystem: ActorSystem[_] = akka.actor.ActorSystem("service", opts.config).toTyped
}
private def terminate(context: AkkaContext): ZIO[Console, Nothing, Unit] = {
console.putStrLn("Terminating actor system").flatMap { _ =>
ZIO
.fromFuture { implicit ec =>
context.actorSystem.toClassic.terminate()
}
.unit
.catchAll(logFatalError)
}
}
val managed = ZManaged.make[Console with ServiceSpecificOptions, Throwable, AkkaContext](create)(terminate)
To work with actors I built some helper functions in a form of extension methods that makes it possible to do some operations as ZIO effects:
- spawn top level actors
- perform an ask as an async ZIO operation
- run a ZIO effect and pipe back its result to an actor
To spawn an actor we need the AkkaContext
trait in environment and the Interop._
extension methods in scope:
for {
system <- actorSystem
actor <- system.spawn(TestActor.create(), "test-actor")
} yield actor
The idea is that the actor itself should also get its dependencies from ZIO, so the TestActor.create()
function itself
is also an effectful function:
object TestActor {
type Environment = ZioDep with PureDep // A subset of the final environment required by the actor
def create[R <: Environment]()(implicit interop: Interop[R]): ZIO[Environment, Nothing, Behavior[Message]] =
ZIO.environment.map(env => new TestActor(env).start())
sealed trait Message
// ...
}
Then we can use the ask pattern to call an actor from ZIO:
for {
testAnswer <- actor.ask[FinalEnvironment, Try[Answer]](TestActor.Question(100, _), 1.second)
_ <- console.putStrLn(s"Actor answered with: $testAnswer")
} yield ()
and we can run ZIO effects from the actor using the pipeTo
extension method:
class TestActor(env: TestActor.Environment) // Subset of the ZIO environment required by the actor
(implicit interop: Interop[TestActor.Environment]) { // needed for the ZIO pipeTo syntax
import TestActor._
def start(): Behavior[Message] =
Behaviors.receive { (ctx, msg) =>
implicit val ec: ExecutionContext = ctx.executionContext
msg match {
case Question(input, respondTo) =>
// ZIO value is evaluated concurrently and the result is sent back to the actor as a message
env.zioDep.provideAnswer(input).pipeTo(ctx.self, AnswerReady(_, respondTo))
Behaviors.same
// ...
}
Note the implicit Interop
value. This is created during bootstrap and must be passed around to non-ZIO places as
it cannot be part of the environment (in fact it is the thing holding the runtime
that holds the environment).
Akka-HTTP is integrated in a very similar way. Dependencies are injected by having a ZIO function that creates the
Api
value, holding the Akka-HTTP route:
def createHttpApi(interopImpl: Interop[FinalEnvironment],
testActor: ActorRef[TestActor.Message]): ZIO[FinalEnvironment, Nothing, Api]
Api
uses the structure that we are using in our current Akka-HTTP services too where different route fragments are
mixed in together:
val route: Route = futureRoute ~ catsRoute ~