Skip to content

Example of simultaneously using ZIO and Akka to implement a backend service

Notifications You must be signed in to change notification settings

vigoo/zio-akka-service-template

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zio-service-template

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.

Bootstrap

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.

Context

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)

Working with actors

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

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 ~